Move Argeo SLC JCR components to Argeo JCR
authorMathieu Baudier <mbaudier@argeo.org>
Mon, 4 Dec 2023 13:50:47 +0000 (14:50 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Mon, 4 Dec 2023 13:50:47 +0000 (14:50 +0100)
425 files changed:
Makefile
org.argeo.slc.jcr/.classpath [new file with mode: 0644]
org.argeo.slc.jcr/.gitignore [new file with mode: 0644]
org.argeo.slc.jcr/.project [new file with mode: 0644]
org.argeo.slc.jcr/META-INF/.gitignore [new file with mode: 0644]
org.argeo.slc.jcr/bnd.bnd [new file with mode: 0644]
org.argeo.slc.jcr/build.properties [new file with mode: 0644]
org.argeo.slc.jcr/src/org/argeo/cli/jcr/JcrCommands.java [new file with mode: 0644]
org.argeo.slc.jcr/src/org/argeo/cli/jcr/JcrSync.java [new file with mode: 0644]
org.argeo.slc.jcr/src/org/argeo/cli/jcr/package-info.java [new file with mode: 0644]
org.argeo.slc.jcr/src/org/argeo/cli/jcr/repository-localfs.xml [new file with mode: 0644]
org.argeo.slc.jcr/src/org/argeo/slc/jcr/JcrMetadataWriter.java [new file with mode: 0644]
org.argeo.slc.jcr/src/org/argeo/slc/jcr/JcrTestResult.java [new file with mode: 0644]
org.argeo.slc.jcr/src/org/argeo/slc/jcr/SlcJcrConstants.java [new file with mode: 0644]
org.argeo.slc.jcr/src/org/argeo/slc/jcr/SlcJcrResultUtils.java [new file with mode: 0644]
org.argeo.slc.jcr/src/org/argeo/slc/jcr/SlcJcrUtils.java [new file with mode: 0644]
org.argeo.slc.jcr/src/org/argeo/slc/jcr/execution/JcrAgent.java [new file with mode: 0644]
org.argeo.slc.jcr/src/org/argeo/slc/jcr/execution/JcrExecutionModulesListener.java [new file with mode: 0644]
org.argeo.slc.jcr/src/org/argeo/slc/jcr/execution/JcrExecutionProcess.java [new file with mode: 0644]
org.argeo.slc.jcr/src/org/argeo/slc/jcr/execution/JcrProcessThread.java [new file with mode: 0644]
org.argeo.slc.jcr/src/org/argeo/slc/jcr/execution/JcrRealizedFlow.java [new file with mode: 0644]
org.argeo.slc.repo/.classpath [new file with mode: 0644]
org.argeo.slc.repo/.gitignore [new file with mode: 0644]
org.argeo.slc.repo/.project [new file with mode: 0644]
org.argeo.slc.repo/META-INF/.gitignore [new file with mode: 0644]
org.argeo.slc.repo/bnd.bnd [new file with mode: 0644]
org.argeo.slc.repo/build.properties [new file with mode: 0644]
org.argeo.slc.repo/ext/test/org/argeo/slc/repo/internal/AetherUtilsTest.java [new file with mode: 0644]
org.argeo.slc.repo/ext/test/org/argeo/slc/repo/internal/pom.xml [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/ArgeoOsgiDistribution.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/ArtifactDistribution.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/ArtifactIndexer.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/FreeLicense.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/JarFileIndexer.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/JavaRepoManager.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/MavenProxyService.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/ModularDistributionFactory.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/ModularDistributionIndexer.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/NodeIndexer.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/NodeIndexerVisitor.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/OsgiBundlesProvider.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/OsgiFactory.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/PdeSourcesIndexer.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/RepoConstants.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/RepoService.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/RepoSync.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/RepoUtils.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/RpmRepoManager.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/SlcRepoManager.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/core/AbstractJcrRepoManager.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/core/JavaRepoManagerImpl.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/core/RepoServiceImpl.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/core/RpmRepoManagerImpl.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/core/SlcRepoManagerImpl.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/core/WorkspaceIndexer.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/internal/springutil/AntPathMatcher.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/internal/springutil/PathMatcher.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/license/apache-2.0.txt [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/license/bsd-3-clause.txt [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/license/cddl-1.0.txt [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/license/epl-1.0.txt [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/license/gpl-2.0.txt [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/license/gpl-3.0.txt [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/license/lgpl-2.1.txt [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/license/lgpl-3.0.txt [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/license/mit.txt [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/maven/AetherUtils.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/maven/ArtifactIdComparator.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/maven/ConvertPoms_01_03.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/maven/GenerateBinaries.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/maven/IndexDistribution.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/maven/MavenConventionsUtils.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/maven/MavenProxyServiceImpl.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/maven/Migration_01_03.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ArchiveSourcesProvider.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ArchiveWrapper.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ArchiveWrapperCNV.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ArgeoOsgiDistributionImpl.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/BndWrapper.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ImportBundlesZip.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/JavaSE-1.6.profile [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/JavaSE-1.7.profile [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/MavenWrapper.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/NormalizeGroup.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ObrWrapper.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/OsgiFactoryImpl.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/OsgiProfile.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ProcessDistribution.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/SourcesProvider.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/SubArtifact.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/UriWrapper.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/argeo/slc/repo/repo.cnd [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/AbstractForwardingRepositorySystemSession.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/AbstractRepositoryListener.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/ConfigurationProperties.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/DefaultRepositoryCache.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/DefaultRepositorySystemSession.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/DefaultSessionData.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/RepositoryCache.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/RepositoryEvent.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/RepositoryException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/RepositoryListener.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/RepositorySystem.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/RepositorySystemSession.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/RequestTrace.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/SessionData.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/SyncContext.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/artifact/AbstractArtifact.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/artifact/Artifact.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/artifact/ArtifactProperties.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/artifact/ArtifactType.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/artifact/ArtifactTypeRegistry.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/artifact/DefaultArtifact.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/artifact/DefaultArtifactType.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/artifact/package-info.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/collection/CollectRequest.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/collection/CollectResult.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyCollectionContext.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyCollectionException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyGraphTransformationContext.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyGraphTransformer.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyManagement.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyManager.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencySelector.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyTraverser.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/collection/UnsolvableVersionConflictException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/collection/VersionFilter.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/collection/package-info.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/deployment/DeployRequest.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/deployment/DeployResult.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/deployment/DeploymentException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/deployment/package-info.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/graph/DefaultDependencyNode.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/graph/Dependency.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/graph/DependencyCycle.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/graph/DependencyFilter.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/graph/DependencyNode.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/graph/DependencyVisitor.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/graph/Exclusion.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/graph/package-info.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/installation/InstallRequest.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/installation/InstallResult.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/installation/InstallationException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/installation/package-info.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/metadata/AbstractMetadata.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/metadata/DefaultMetadata.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/metadata/MergeableMetadata.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/metadata/Metadata.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/metadata/package-info.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/package-info.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/ArtifactRepository.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/Authentication.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/AuthenticationContext.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/AuthenticationDigest.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/AuthenticationSelector.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalArtifactRegistration.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalArtifactRequest.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalArtifactResult.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalMetadataRegistration.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalMetadataRequest.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalMetadataResult.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalRepository.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalRepositoryManager.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/MirrorSelector.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/NoLocalRepositoryManagerException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/Proxy.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/ProxySelector.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/RemoteRepository.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/RepositoryPolicy.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/WorkspaceReader.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/WorkspaceRepository.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/repository/package-info.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactDescriptorException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactDescriptorPolicy.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactDescriptorPolicyRequest.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactDescriptorRequest.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactDescriptorResult.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactRequest.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactResolutionException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactResult.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/DependencyRequest.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/DependencyResolutionException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/DependencyResult.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/MetadataRequest.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/MetadataResult.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/ResolutionErrorPolicy.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/ResolutionErrorPolicyRequest.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionRangeRequest.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionRangeResolutionException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionRangeResult.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionRequest.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionResolutionException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionResult.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/resolution/package-info.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/transfer/AbstractTransferListener.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/transfer/ArtifactNotFoundException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/transfer/ArtifactTransferException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/transfer/ChecksumFailureException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/transfer/MetadataNotFoundException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/transfer/MetadataTransferException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/transfer/NoRepositoryConnectorException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/transfer/NoRepositoryLayoutException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/transfer/NoTransporterException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/transfer/RepositoryOfflineException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/transfer/TransferCancelledException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/transfer/TransferEvent.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/transfer/TransferListener.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/transfer/TransferResource.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/transfer/package-info.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/version/InvalidVersionSpecificationException.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/version/Version.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/version/VersionConstraint.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/version/VersionRange.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/version/VersionScheme.java [new file with mode: 0644]
org.argeo.slc.repo/src/org/eclipse/aether/version/package-info.java [new file with mode: 0644]
org.argeo.slc.rpmfactory/.classpath [new file with mode: 0644]
org.argeo.slc.rpmfactory/.gitignore [new file with mode: 0644]
org.argeo.slc.rpmfactory/.project [new file with mode: 0644]
org.argeo.slc.rpmfactory/META-INF/.gitignore [new file with mode: 0644]
org.argeo.slc.rpmfactory/bnd.bnd [new file with mode: 0644]
org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/RpmFactory.java [new file with mode: 0644]
org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/RpmProxyService.java [new file with mode: 0644]
org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/RpmRepository.java [new file with mode: 0644]
org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/AbstractRpmRepository.java [new file with mode: 0644]
org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/BuildInMock.java [new file with mode: 0644]
org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/CreateRpmDistribution.java [new file with mode: 0644]
org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/ReleaseStaging.java [new file with mode: 0644]
org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmDistribution.java [new file with mode: 0644]
org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmFactoryImpl.java [new file with mode: 0644]
org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmIndexer.java [new file with mode: 0644]
org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmPackageSet.java [new file with mode: 0644]
org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmProxyServiceImpl.java [new file with mode: 0644]
org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmSpecFile.java [new file with mode: 0644]
org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/StagingRpmRepository.java [new file with mode: 0644]
org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/ThirdPartyRpmRepository.java [new file with mode: 0644]
org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/YumListParser.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/.classpath [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/.project [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/OSGI-INF/cmsAdminRap.xml [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/OSGI-INF/homeRepository.xml [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/OSGI-INF/userAdminWrapper.xml [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/bnd.bnd [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/build.properties [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/e4xmi/devops.e4xmi [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/files/NodeFsBrowserView.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/files/package-info.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/EclipseJcrMonitor.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/GenericPropertyPage.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrBrowserView.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrE4DClickListener.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrNodeEditor.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/SimplePart.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/AddFolderNode.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/AddRemoteRepository.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/DeleteNodes.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/Refresh.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/RenameNode.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/package-info.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/package-info.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/AbstractOsgiComposite.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/Browse.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/ConnectivityDeploymentUi.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/DataDeploymentUi.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/DeploymentEntryPoint.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/LogDeploymentUi.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/MaintenanceStyles.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/NonAdminPage.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/SecurityDeploymentUi.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/package-info.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/BundleNode.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/BundlesView.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/CmsSessionsView.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/ModulesView.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/OsgiConfigurationsView.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/OsgiExplorerImages.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/ServiceReferenceNode.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/StateLabelProvider.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/package-info.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/parts/EgoDashboard.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/AbstractRoleEditor.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/CmsWorkbenchStyles.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/GroupEditor.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/GroupsView.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/SecurityAdminImages.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UiAdminUtils.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UiUserAdminListener.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserAdminWrapper.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserBatchUpdateWizard.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserEditor.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserTableDefaultDClickListener.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UsersView.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/DeleteGroups.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/DeleteUsers.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/NewGroup.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/NewUser.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/package-info.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/package-info.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/CommonNameLP.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/DomainNameLP.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/MailLP.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/RoleIconLP.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserAdminAbstractLP.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserDragListener.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserFilter.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserNameLP.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/package-info.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/jcr/e4/rap/CmsE4AdminApp.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/LdifUsersTable.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/PickUpUserDialog.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/UserLP.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/UsersImages.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/ViewerUtils.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/package-info.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/AbstractFormPart.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormColors.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormFonts.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormToolkit.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormUtil.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IFormColors.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IFormPart.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IManagedForm.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IPartSelectionListener.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/ManagedForm.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/FormEditor.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/FormPage.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/IFormPage.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/DefaultRepositoryRegister.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/FullVersioningTreeContentProvider.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrBrowserUtils.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrDClickListener.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrImages.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrTreeContentProvider.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/NodeContentProvider.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/NodeLabelProvider.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/OsgiRepositoryRegister.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/PropertiesContentProvider.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/PropertyLabelProvider.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/RepositoryRegister.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/VersionLabelProvider.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/MaintainedRepositoryElem.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RemoteRepositoryElem.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RepositoriesElem.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RepositoryElem.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/SingleJcrNodeElem.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/WorkspaceElem.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/package-info.java [new file with mode: 0644]
swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/package-info.java [new file with mode: 0644]
swt/org.argeo.tool.swt/.classpath [new file with mode: 0644]
swt/org.argeo.tool.swt/.project [new file with mode: 0644]
swt/org.argeo.tool.swt/bnd.bnd [new file with mode: 0644]
swt/org.argeo.tool.swt/build.properties [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/actions/add.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/actions/close-all.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/actions/delete.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/actions/edit.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/actions/save-all.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/actions/save.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/active.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/add.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/add.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/addFolder.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/addPrivileges.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/addRepo.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/addWorkspace.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/adminLog.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/batch.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/begin.gif [new file with mode: 0755]
swt/org.argeo.tool.swt/icons/binary.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/browser.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/bundles.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/changePassword.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/clear.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/close-all.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/commit.gif [new file with mode: 0755]
swt/org.argeo.tool.swt/icons/delete.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/dumpNode.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/file.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/folder.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/getSize.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/group.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/home.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/home.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/import_fs.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/installed.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/log.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/logout.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/maintenance.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/node.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/nodes.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/osgi_explorer.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/password.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/person-logged-in.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/person.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/query.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/refresh.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/remote_connected.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/remote_disconnected.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/remove.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/removePrivileges.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/rename.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/repositories.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/repository_connected.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/repository_disconnected.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/resolved.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/role.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/rollback.gif [new file with mode: 0755]
swt/org.argeo.tool.swt/icons/save-all.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/save.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/save.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/save_security.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/save_security_disabled.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/security.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/service_published.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/service_referenced.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/sort.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/starting.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/sync.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/user.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/users.gif [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/workgroup.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/workgroup.xcf [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/workspace_connected.png [new file with mode: 0644]
swt/org.argeo.tool.swt/icons/workspace_disconnected.png [new file with mode: 0644]
swt/org.argeo.tool.swt/src/org/argeo/cms/ui/theme/CmsImages.java [new file with mode: 0644]
swt/org.argeo.tool.swt/src/org/argeo/cms/ui/theme/package-info.java [new file with mode: 0644]

index 6fde8b6d626ac3f2b14a1f4b78a5305b1173cab2..e2ee81fe5ca14f688bce21f1eef3876eb6b096ce 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -10,18 +10,29 @@ A2_CATEGORY = org.argeo.cms.jcr
 
 BUNDLES = \
 org.argeo.cms.jcr \
+org.argeo.slc.repo \
+org.argeo.slc.rpmfactory \
+org.argeo.slc.jcr \
 swt/org.argeo.cms.jcr.ui \
+swt/org.argeo.tool.swt \
+swt/org.argeo.tool.devops.e4 \
 
 DEP_CATEGORIES = \
 org.argeo.tp \
+org.argeo.tp.build \
 org.argeo.tp.jcr \
+org.argeo.tp.sdk \
+org.argeo.tp.utils \
 osgi/equinox/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.cms \
+org.argeo.slc \
 swt/org.argeo.cms \
+swt/org.argeo.slc \
 swt/rap/org.argeo.cms \
+swt/rap/org.argeo.slc \
 $(A2_CATEGORY)
 
 clean:
diff --git a/org.argeo.slc.jcr/.classpath b/org.argeo.slc.jcr/.classpath
new file mode 100644 (file)
index 0000000..e801ebf
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.slc.jcr/.gitignore b/org.argeo.slc.jcr/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/org.argeo.slc.jcr/.project b/org.argeo.slc.jcr/.project
new file mode 100644 (file)
index 0000000..a4cd874
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.slc.jcr</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.slc.jcr/META-INF/.gitignore b/org.argeo.slc.jcr/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.slc.jcr/bnd.bnd b/org.argeo.slc.jcr/bnd.bnd
new file mode 100644 (file)
index 0000000..725fe55
--- /dev/null
@@ -0,0 +1,5 @@
+Import-Package: javax.jcr.nodetype,\
+javax.jcr.security,\
+org.apache.jackrabbit.api,\
+org.apache.jackrabbit.commons,\
+*
\ No newline at end of file
diff --git a/org.argeo.slc.jcr/build.properties b/org.argeo.slc.jcr/build.properties
new file mode 100644 (file)
index 0000000..ffcf290
--- /dev/null
@@ -0,0 +1,21 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .
+additional.bundles = org.junit,\
+                     org.apache.jackrabbit.core,\
+                     javax.jcr,\
+                     org.apache.jackrabbit.api,\
+                     org.apache.jackrabbit.jcr.commons,\
+                     org.apache.jackrabbit.spi,\
+                     org.apache.jackrabbit.spi.commons,\
+                     org.argeo.tp.syslogger,\
+                     org.apache.commons.collections,\
+                     EDU.oswego.cs.dl.util.concurrent,\
+                     org.apache.lucene,\
+                     org.apache.tika,\
+                     org.apache.commons.dbcp,\
+                     org.apache.jackrabbit.jcr2spi,\
+                     org.apache.jackrabbit.spi2dav,\
+                     org.apache.httpcomponents.httpclient,\
+                     org.apache.httpcomponents.httpcore
\ No newline at end of file
diff --git a/org.argeo.slc.jcr/src/org/argeo/cli/jcr/JcrCommands.java b/org.argeo.slc.jcr/src/org/argeo/cli/jcr/JcrCommands.java
new file mode 100644 (file)
index 0000000..fd5b5d7
--- /dev/null
@@ -0,0 +1,18 @@
+package org.argeo.cli.jcr;
+
+import org.argeo.api.cli.CommandsCli;
+
+/** File utilities. */
+public class JcrCommands extends CommandsCli {
+
+       public JcrCommands(String commandName) {
+               super(commandName);
+               addCommand("sync", new JcrSync());
+       }
+
+       @Override
+       public String getDescription() {
+               return "Utilities around remote and local JCR repositories";
+       }
+
+}
diff --git a/org.argeo.slc.jcr/src/org/argeo/cli/jcr/JcrSync.java b/org.argeo.slc.jcr/src/org/argeo/cli/jcr/JcrSync.java
new file mode 100644 (file)
index 0000000..ed1a5f8
--- /dev/null
@@ -0,0 +1,133 @@
+package org.argeo.cli.jcr;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.Credentials;
+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.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.apache.jackrabbit.core.config.RepositoryConfig;
+import org.argeo.api.cli.CommandArgsException;
+import org.argeo.api.cli.CommandRuntimeException;
+import org.argeo.api.cli.DescribedCommand;
+import org.argeo.cms.file.SyncResult;
+import org.argeo.jackrabbit.client.ClientDavexRepositoryFactory;
+import org.argeo.jcr.JcrUtils;
+
+public class JcrSync implements DescribedCommand<SyncResult<Node>> {
+       public final static String DEFAULT_LOCALFS_CONFIG = "repository-localfs.xml";
+
+       final static Option deleteOption = Option.builder().longOpt("delete").desc("delete from target").build();
+       final static Option recursiveOption = Option.builder("r").longOpt("recursive").desc("recurse into directories")
+                       .build();
+       final static Option progressOption = Option.builder().longOpt("progress").hasArg(false).desc("show progress")
+                       .build();
+
+       @Override
+       public SyncResult<Node> apply(List<String> t) {
+               try {
+                       CommandLine line = toCommandLine(t);
+                       List<String> remaining = line.getArgList();
+                       if (remaining.size() == 0) {
+                               throw new CommandArgsException("There must be at least one argument");
+                       }
+                       URI sourceUri = new URI(remaining.get(0));
+                       URI targetUri;
+                       if (remaining.size() == 1) {
+                               targetUri = Paths.get(System.getProperty("user.dir")).toUri();
+                       } else {
+                               targetUri = new URI(remaining.get(1));
+                       }
+                       boolean delete = line.hasOption(deleteOption.getLongOpt());
+                       boolean recursive = line.hasOption(recursiveOption.getLongOpt());
+
+                       // TODO make it configurable
+                       String sourceWorkspace = "home";
+                       String targetWorkspace = sourceWorkspace;
+
+                       final Repository sourceRepository;
+                       final Session sourceSession;
+                       Credentials sourceCredentials = null;
+                       final Repository targetRepository;
+                       final Session targetSession;
+                       Credentials targetCredentials = null;
+
+                       if ("http".equals(sourceUri.getScheme()) || "https".equals(sourceUri.getScheme())) {
+                               sourceRepository = createRemoteRepository(sourceUri);
+                       } else if (null == sourceUri.getScheme() || "file".equals(sourceUri.getScheme())) {
+                               RepositoryConfig repositoryConfig = RepositoryConfig.create(
+                                               JcrSync.class.getResourceAsStream(DEFAULT_LOCALFS_CONFIG), sourceUri.getPath().toString());
+                               sourceRepository = RepositoryImpl.create(repositoryConfig);
+                               sourceCredentials = new SimpleCredentials("admin", "admin".toCharArray());
+                       } else {
+                               throw new IllegalArgumentException("Unsupported scheme " + sourceUri.getScheme());
+                       }
+                       sourceSession = JcrUtils.loginOrCreateWorkspace(sourceRepository, sourceWorkspace, sourceCredentials);
+
+                       if ("http".equals(targetUri.getScheme()) || "https".equals(targetUri.getScheme())) {
+                               targetRepository = createRemoteRepository(targetUri);
+                       } else if (null == targetUri.getScheme() || "file".equals(targetUri.getScheme())) {
+                               RepositoryConfig repositoryConfig = RepositoryConfig.create(
+                                               JcrSync.class.getResourceAsStream(DEFAULT_LOCALFS_CONFIG), targetUri.getPath().toString());
+                               targetRepository = RepositoryImpl.create(repositoryConfig);
+                               targetCredentials = new SimpleCredentials("admin", "admin".toCharArray());
+                       } else {
+                               throw new IllegalArgumentException("Unsupported scheme " + targetUri.getScheme());
+                       }
+                       targetSession = JcrUtils.loginOrCreateWorkspace(targetRepository, targetWorkspace, targetCredentials);
+
+                       JcrUtils.copy(sourceSession.getRootNode(), targetSession.getRootNode());
+                       return new SyncResult<Node>();
+               } catch (URISyntaxException e) {
+                       throw new CommandArgsException(e);
+               } catch (Exception e) {
+                       throw new CommandRuntimeException(e, this, t);
+               }
+       }
+
+       protected Repository createRemoteRepository(URI uri) throws RepositoryException {
+               RepositoryFactory repositoryFactory = new ClientDavexRepositoryFactory();
+               Map<String, String> params = new HashMap<String, String>();
+               params.put(ClientDavexRepositoryFactory.JACKRABBIT_DAVEX_URI, uri.toString());
+               // FIXME make it configurable
+               params.put(ClientDavexRepositoryFactory.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, "sys");
+               return repositoryFactory.getRepository(params);
+       }
+
+       @Override
+       public Options getOptions() {
+               Options options = new Options();
+               options.addOption(recursiveOption);
+               options.addOption(deleteOption);
+               options.addOption(progressOption);
+               return options;
+       }
+
+       @Override
+       public String getUsage() {
+               return "[source URI] [target URI]";
+       }
+
+       public static void main(String[] args) {
+               DescribedCommand.mainImpl(new JcrSync(), args);
+       }
+
+       @Override
+       public String getDescription() {
+               return "Synchronises JCR repositories";
+       }
+
+}
diff --git a/org.argeo.slc.jcr/src/org/argeo/cli/jcr/package-info.java b/org.argeo.slc.jcr/src/org/argeo/cli/jcr/package-info.java
new file mode 100644 (file)
index 0000000..6f3f01f
--- /dev/null
@@ -0,0 +1,2 @@
+/** JCR CLI commands. */
+package org.argeo.cli.jcr;
\ No newline at end of file
diff --git a/org.argeo.slc.jcr/src/org/argeo/cli/jcr/repository-localfs.xml b/org.argeo.slc.jcr/src/org/argeo/cli/jcr/repository-localfs.xml
new file mode 100644 (file)
index 0000000..5e7759c
--- /dev/null
@@ -0,0 +1,76 @@
+<?xml version="1.0"?>
+<!--
+
+    Copyright (C) 2007-2012 Argeo GmbH
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+            http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<!DOCTYPE Repository PUBLIC "-//The Apache Software Foundation//DTD Jackrabbit 1.6//EN"
+                            "http://jackrabbit.apache.org/dtd/repository-2.0.dtd">
+<Repository>
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+               <param name="path" value="${rep.home}/repository" />
+       </FileSystem>
+       <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+               <param name="path" value="${rep.home}/datastore" />
+       </DataStore>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="main" configRootPath="/workspaces" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+                       <param name="path" value="${wsp.home}" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${rep.home}/repository/index" />
+               </SearchIndex>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+                       <param name="path" value="${rep.home}/version" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/repository/index" />
+               <param name="tikaConfigPath" value="tika-config.xml"/>
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager
+                       class="org.apache.jackrabbit.core.security.simple.SimpleSecurityManager"
+                       workspaceName="security" />
+               <AccessManager
+                       class="org.apache.jackrabbit.core.security.simple.SimpleAccessManager" />
+               <LoginModule
+                       class="org.apache.jackrabbit.core.security.simple.SimpleLoginModule">
+                       <param name="anonymousId" value="anonymous" />
+                       <param name="adminId" value="admin" />
+               </LoginModule>
+       </Security>
+</Repository>
\ No newline at end of file
diff --git a/org.argeo.slc.jcr/src/org/argeo/slc/jcr/JcrMetadataWriter.java b/org.argeo.slc.jcr/src/org/argeo/slc/jcr/JcrMetadataWriter.java
new file mode 100644 (file)
index 0000000..f50b9f8
--- /dev/null
@@ -0,0 +1,62 @@
+package org.argeo.slc.jcr;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+
+/**
+ * Writes arbitrary metadata into a child node of a given node (or the node
+ * itself if metadata node name is set to null)
+ */
+public class JcrMetadataWriter implements Runnable {
+       private final static CmsLog log = CmsLog.getLog(JcrMetadataWriter.class);
+
+       private Node baseNode;
+       private String metadataNodeName = SlcNames.SLC_METADATA;
+
+       private Map<String, String> metadata = new HashMap<String, String>();
+
+       public void run() {
+               try {
+                       Node metadataNode;
+                       if (metadataNodeName != null)
+                               metadataNode = baseNode.hasNode(metadataNodeName) ? baseNode.getNode(metadataNodeName)
+                                               : baseNode.addNode(metadataNodeName);
+                       else
+                               metadataNode = baseNode;
+
+                       for (String key : metadata.keySet())
+                               metadataNode.setProperty(key, metadata.get(key));
+
+                       baseNode.getSession().save();
+
+                       if (log.isDebugEnabled())
+                               log.debug("Wrote " + metadata.size() + " metadata entries to " + metadataNode);
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot write metadata to " + baseNode, e);
+               } finally {
+                       JcrUtils.discardUnderlyingSessionQuietly(baseNode);
+               }
+
+       }
+
+       public void setBaseNode(Node baseNode) {
+               this.baseNode = baseNode;
+       }
+
+       public void setMetadataNodeName(String metadataNodeName) {
+               this.metadataNodeName = metadataNodeName;
+       }
+
+       public void setMetadata(Map<String, String> metadata) {
+               this.metadata = metadata;
+       }
+
+}
diff --git a/org.argeo.slc.jcr/src/org/argeo/slc/jcr/JcrTestResult.java b/org.argeo.slc.jcr/src/org/argeo/slc/jcr/JcrTestResult.java
new file mode 100644 (file)
index 0000000..72d0b4d
--- /dev/null
@@ -0,0 +1,275 @@
+package org.argeo.slc.jcr;
+
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.jcr.Credentials;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.Repository;
+import javax.jcr.Session;
+import javax.jcr.query.Query;
+import javax.jcr.query.QueryManager;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.argeo.slc.attachment.Attachment;
+import org.argeo.slc.attachment.AttachmentsEnabled;
+import org.argeo.slc.test.TestResult;
+import org.argeo.slc.test.TestResultPart;
+import org.argeo.slc.test.TestRun;
+import org.argeo.slc.test.TestStatus;
+
+/**
+ * {@link TestResult} wrapping a JCR node of type
+ * {@link SlcTypes#SLC_TEST_RESULT}.
+ */
+public class JcrTestResult implements TestResult, SlcNames, AttachmentsEnabled {
+       private final static CmsLog log = CmsLog.getLog(JcrTestResult.class);
+
+       /** Should only be set for an already existing result. */
+       private String uuid;
+       private Repository repository;
+       private Session session;
+       /**
+        * For testing purposes, best practice is to not set them explicitely but
+        * via other mechanisms such as JAAS or SPring Security.
+        */
+       private Credentials credentials = null;
+       private String resultType = SlcTypes.SLC_TEST_RESULT;
+
+       /** cached for performance purposes */
+       private String nodeIdentifier = null;
+
+       private Map<String, String> attributes = new HashMap<String, String>();
+
+       public void init() {
+               try {
+                       session = repository.login(credentials);
+                       if (uuid == null) {
+                               // create new result
+                               uuid = UUID.randomUUID().toString();
+                               String path = SlcJcrUtils.createResultPath(session, uuid);
+                               Node resultNode = JcrUtils.mkdirs(session, path, resultType);
+                               resultNode.setProperty(SLC_UUID, uuid);
+                               for (String attr : attributes.keySet()) {
+                                       String property = attr;
+                                       // compatibility with legacy applications
+                                       if ("testCase".equals(attr))
+                                               property = SLC_TEST_CASE;
+                                       else if ("testCaseType".equals(attr))
+                                               property = SLC_TEST_CASE_TYPE;
+                                       resultNode.setProperty(property, attributes.get(attr));
+                               }
+                               session.save();
+                               if (log.isDebugEnabled())
+                                       log.debug("Created test result " + uuid);
+                       }
+               } catch (Exception e) {
+                       JcrUtils.discardQuietly(session);
+                       throw new SlcException("Cannot initialize JCR result", e);
+               }
+       }
+
+       public void destroy() {
+               JcrUtils.logoutQuietly(session);
+               if (log.isTraceEnabled())
+                       log.trace("Logged out session for result " + uuid);
+       }
+
+       public Node getNode() {
+               try {
+                       Node resultNode;
+                       if (nodeIdentifier != null) {
+                               return session.getNodeByIdentifier(nodeIdentifier);
+                       } else {
+                               QueryManager qm = session.getWorkspace().getQueryManager();
+                               Query q = qm.createQuery("select * from ["
+                                               + SlcTypes.SLC_TEST_RESULT + "] where [slc:uuid]='"
+                                               + uuid + "'", Query.JCR_SQL2);
+                               resultNode = JcrUtils.querySingleNode(q);
+                               if (resultNode != null)
+                                       nodeIdentifier = resultNode.getIdentifier();
+                       }
+                       return resultNode;
+               } catch (Exception e) {
+                       throw new SlcException("Cannot get result node", e);
+               }
+       }
+
+       public void notifyTestRun(TestRun testRun) {
+               // TODO store meta data about the test running
+               // if (log.isDebugEnabled())
+               // log.debug("Running test "
+               // + testRun.getTestDefinition().getClass().getName() + "...");
+       }
+
+       public void addResultPart(TestResultPart testResultPart) {
+               Node node = getNode();
+
+               try {
+                       // error : revert all unsaved changes on the resultNode to be sure
+                       // it is in a consistant state
+                       if (testResultPart.getExceptionMessage() != null)
+                               JcrUtils.discardQuietly(node.getSession());
+                       node.getSession().save();
+
+                       // add the new result part, retrieving status information
+                       Node resultPartNode = node.addNode(SlcNames.SLC_RESULT_PART,
+                                       SlcTypes.SLC_CHECK);
+                       resultPartNode.setProperty(SLC_SUCCESS, testResultPart.getStatus()
+                                       .equals(TestStatus.PASSED));
+                       if (testResultPart.getMessage() != null)
+                               resultPartNode.setProperty(SLC_MESSAGE,
+                                               testResultPart.getMessage());
+                       if (testResultPart.getStatus().equals(TestStatus.ERROR)) {
+                               resultPartNode.setProperty(SLC_ERROR_MESSAGE,
+                                               (testResultPart.getExceptionMessage() == null) ? ""
+                                                               : testResultPart.getExceptionMessage());
+                       }
+
+                       // helper update aggregate status node
+                       Node mainStatus;
+                       if (!node.hasNode(SLC_AGGREGATED_STATUS)) {
+
+                               mainStatus = node.addNode(SLC_AGGREGATED_STATUS,
+                                               SlcTypes.SLC_CHECK);
+                               mainStatus.setProperty(SLC_SUCCESS,
+                                               resultPartNode.getProperty(SLC_SUCCESS).getBoolean());
+                               if (resultPartNode.hasProperty(SLC_MESSAGE))
+                                       mainStatus.setProperty(SLC_MESSAGE, resultPartNode
+                                                       .getProperty(SLC_MESSAGE).getString());
+                               if (resultPartNode.hasProperty(SLC_ERROR_MESSAGE))
+                                       mainStatus.setProperty(SLC_ERROR_MESSAGE, resultPartNode
+                                                       .getProperty(SLC_ERROR_MESSAGE).getString());
+                       } else {
+                               mainStatus = node.getNode(SLC_AGGREGATED_STATUS);
+                               if (mainStatus.hasProperty(SLC_ERROR_MESSAGE)) {
+                                       // main status already in error we do nothing
+                               } else if (resultPartNode.hasProperty(SLC_ERROR_MESSAGE)) {
+                                       // main status was not in error and new result part is in
+                                       // error; we update main status
+                                       mainStatus.setProperty(SLC_SUCCESS, false);
+                                       mainStatus.setProperty(SLC_ERROR_MESSAGE, resultPartNode
+                                                       .getProperty(SLC_ERROR_MESSAGE).getString());
+                                       if (resultPartNode.hasProperty(SLC_MESSAGE))
+                                               mainStatus.setProperty(SLC_MESSAGE, resultPartNode
+                                                               .getProperty(SLC_MESSAGE).getString());
+                                       else
+                                               // remove old message to remain consistent
+                                               mainStatus.setProperty(SLC_MESSAGE, "");
+                               } else if (!mainStatus.getProperty(SLC_SUCCESS).getBoolean()) {
+                                       // main status was already failed and new result part is not
+                                       // in error, we do nothing
+                               } else if (!resultPartNode.getProperty(SLC_SUCCESS)
+                                               .getBoolean()) {
+                                       // new resultPart that is failed
+                                       mainStatus.setProperty(SLC_SUCCESS, false);
+                                       if (resultPartNode.hasProperty(SLC_MESSAGE))
+                                               mainStatus.setProperty(SLC_MESSAGE, resultPartNode
+                                                               .getProperty(SLC_MESSAGE).getString());
+                                       else
+                                               // remove old message to remain consistent
+                                               mainStatus.setProperty(SLC_MESSAGE, "");
+                               } else if (resultPartNode.hasProperty(SLC_MESSAGE)
+                                               && (!mainStatus.hasProperty(SLC_MESSAGE) || (""
+                                                               .equals(mainStatus.getProperty(SLC_MESSAGE)
+                                                                               .getString().trim())))) {
+                                       mainStatus.setProperty(SLC_MESSAGE, resultPartNode
+                                                       .getProperty(SLC_MESSAGE).getString());
+                               }
+                       }
+                       JcrUtils.updateLastModified(node);
+                       node.getSession().save();
+               } catch (Exception e) {
+                       JcrUtils.discardUnderlyingSessionQuietly(node);
+                       throw new SlcException("Cannot add ResultPart to node " + node, e);
+               }
+       }
+
+       public String getUuid() {
+               Node node = getNode();
+               try {
+                       return node.getProperty(SLC_UUID).getString();
+               } catch (Exception e) {
+                       throw new SlcException("Cannot get UUID from " + node, e);
+               }
+       }
+
+       /** JCR session is NOT logged out */
+       public void close() {
+               Node node = getNode();
+               try {
+                       if (node.hasNode(SLC_COMPLETED))
+                               return;
+                       node.setProperty(SLC_COMPLETED, new GregorianCalendar());
+                       JcrUtils.updateLastModified(node);
+                       node.getSession().save();
+               } catch (Exception e) {
+                       JcrUtils.discardUnderlyingSessionQuietly(node);
+                       throw new SlcException("Cannot get close date from " + node, e);
+               }
+       }
+
+       public Date getCloseDate() {
+               Node node = getNode();
+               try {
+                       if (!node.hasNode(SLC_COMPLETED))
+                               return null;
+                       return node.getProperty(SLC_COMPLETED).getDate().getTime();
+               } catch (Exception e) {
+                       throw new SlcException("Cannot get close date from " + node, e);
+               }
+       }
+
+       public Map<String, String> getAttributes() {
+               Node node = getNode();
+               try {
+                       Map<String, String> map = new HashMap<String, String>();
+                       PropertyIterator pit = node.getProperties();
+                       while (pit.hasNext()) {
+                               Property p = pit.nextProperty();
+                               if (!p.isMultiple())
+                                       map.put(p.getName(), p.getValue().getString());
+                       }
+                       return map;
+               } catch (Exception e) {
+                       throw new SlcException("Cannot get close date from " + node, e);
+               }
+       }
+
+       public void addAttachment(Attachment attachment) {
+               // TODO implement it
+       }
+
+       public void setUuid(String uuid) {
+               this.uuid = uuid;
+       }
+
+       public void setRepository(Repository repository) {
+               this.repository = repository;
+       }
+
+       public void setResultType(String resultType) {
+               this.resultType = resultType;
+       }
+
+       public void setAttributes(Map<String, String> attributes) {
+               if (uuid != null)
+                       throw new SlcException(
+                                       "Attributes cannot be set on an already initialized test result."
+                                                       + " Update the related JCR node directly instead.");
+               this.attributes = attributes;
+       }
+
+       public void setCredentials(Credentials credentials) {
+               this.credentials = credentials;
+       }
+}
diff --git a/org.argeo.slc.jcr/src/org/argeo/slc/jcr/SlcJcrConstants.java b/org.argeo.slc.jcr/src/org/argeo/slc/jcr/SlcJcrConstants.java
new file mode 100644 (file)
index 0000000..19e6430
--- /dev/null
@@ -0,0 +1,14 @@
+package org.argeo.slc.jcr;
+
+import org.argeo.slc.SlcNames;
+
+/** JCR related constants used across SLC */
+public interface SlcJcrConstants {
+       public final static String PROPERTY_PATH = "argeo.slc.jcr.path";
+
+       public final static String SLC_BASE_PATH = "/" + SlcNames.SLC_SYSTEM;
+       public final static String AGENTS_BASE_PATH = SLC_BASE_PATH + "/"
+                       + SlcNames.SLC_AGENTS;
+       public final static String VM_AGENT_FACTORY_PATH = AGENTS_BASE_PATH + "/"
+                       + SlcNames.SLC_VM;
+}
diff --git a/org.argeo.slc.jcr/src/org/argeo/slc/jcr/SlcJcrResultUtils.java b/org.argeo.slc.jcr/src/org/argeo/slc/jcr/SlcJcrResultUtils.java
new file mode 100644 (file)
index 0000000..219309f
--- /dev/null
@@ -0,0 +1,150 @@
+package org.argeo.slc.jcr;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+
+/**
+ * Utilities around the SLC JCR Result model. Note that it relies on fixed base
+ * paths (convention over configuration) for optimization purposes.
+ */
+public class SlcJcrResultUtils {
+
+       /**
+        * Returns the path to the current slc:result node
+        */
+       public static String getSlcResultsBasePath(Session session) {
+               try {
+                       Node userHome = CmsJcrUtils.getUserHome(session);
+                       if (userHome == null)
+                               throw new SlcException("No user home available for "
+                                               + session.getUserID());
+                       return userHome.getPath() + '/' + SlcNames.SLC_SYSTEM + '/'
+                                       + SlcNames.SLC_RESULTS;
+               } catch (RepositoryException re) {
+                       throw new SlcException(
+                                       "Unexpected error while getting Slc Results Base Path.", re);
+               }
+       }
+
+       /**
+        * Returns the base node to store SlcResults. If it does not exists, it is
+        * created. If a node already exists at the given path with the wrong type,
+        * it throws an exception.
+        * 
+        * @param session
+        */
+       public static Node getSlcResultsParentNode(Session session) {
+               try {
+                       String absPath = getSlcResultsBasePath(session);
+                       if (session.nodeExists(absPath)) {
+                               Node currNode = session.getNode(absPath);
+                               if (currNode.isNodeType(NodeType.NT_UNSTRUCTURED))
+                                       return currNode;
+                               else
+                                       throw new SlcException(
+                                                       "A node already exists at this path : " + absPath
+                                                                       + " that has the wrong type. ");
+                       } else {
+                               Node slcResParNode = JcrUtils.mkdirs(session, absPath);
+                               slcResParNode.setPrimaryType(NodeType.NT_UNSTRUCTURED);
+                               session.save();
+                               return slcResParNode;
+                       }
+               } catch (RepositoryException re) {
+                       throw new SlcException(
+                                       "Unexpected error while creating slcResult root parent node.",
+                                       re);
+               }
+       }
+
+       /**
+        * Returns the path to the current Result UI specific node, depending the
+        * current user
+        */
+       public static String getMyResultsBasePath(Session session) {
+               try {
+                       Node userHome = CmsJcrUtils.getUserHome(session);
+                       if (userHome == null)
+                               throw new SlcException("No user home available for "
+                                               + session.getUserID());
+                       return userHome.getPath() + '/' + SlcNames.SLC_SYSTEM + '/'
+                                       + SlcNames.SLC_MY_RESULTS;
+               } catch (RepositoryException re) {
+                       throw new SlcException(
+                                       "Unexpected error while getting Slc Results Base Path.", re);
+               }
+       }
+
+       /**
+        * Creates a new node with type SlcTypes.SLC_MY_RESULT_ROOT_FOLDER at the
+        * given absolute path. If a node already exists at the given path, returns
+        * that node if it has the correct type and throws an exception otherwise.
+        * 
+        * @param session
+        */
+       public static Node getMyResultParentNode(Session session) {
+               try {
+                       String absPath = getMyResultsBasePath(session);
+                       if (session.nodeExists(absPath)) {
+                               Node currNode = session.getNode(absPath);
+                               if (currNode.isNodeType(SlcTypes.SLC_MY_RESULT_ROOT_FOLDER))
+                                       return currNode;
+                               else
+                                       throw new SlcException(
+                                                       "A node already exists at this path : " + absPath
+                                                                       + " that has the wrong type. ");
+                       } else {
+                               Node myResParNode = JcrUtils.mkdirs(session, absPath);
+                               myResParNode.setPrimaryType(SlcTypes.SLC_MY_RESULT_ROOT_FOLDER);
+                               session.save();
+                               return myResParNode;
+                       }
+               } catch (RepositoryException re) {
+                       throw new SlcException(
+                                       "Unexpected error while creating user MyResult base node.",
+                                       re);
+               }
+       }
+
+       /**
+        * Creates a new node with type SlcTypes.SLC_RESULT_FOLDER at the given
+        * absolute path. If a node already exists at the given path, returns that
+        * node if it has the correct type and throws an exception otherwise.
+        * 
+        * @param session
+        * @param absPath
+        */
+       public static synchronized Node createResultFolderNode(Session session,
+                       String absPath) {
+               try {
+                       if (session.nodeExists(absPath)) {
+                               // Sanity check
+                               Node currNode = session.getNode(absPath);
+                               if (currNode.isNodeType(SlcTypes.SLC_RESULT_FOLDER))
+                                       return currNode;
+                               else
+                                       throw new SlcException(
+                                                       "A node already exists at this path : " + absPath
+                                                                       + " that has the wrong type. ");
+                       }
+                       Node rfNode = JcrUtils.mkdirs(session, absPath);
+                       rfNode.setPrimaryType(SlcTypes.SLC_RESULT_FOLDER);
+                       Node statusNode = rfNode.addNode(SlcNames.SLC_AGGREGATED_STATUS,
+                                       SlcTypes.SLC_CHECK);
+                       statusNode.setProperty(SlcNames.SLC_SUCCESS, true);
+                       session.save();
+                       return rfNode;
+               } catch (RepositoryException re) {
+                       throw new SlcException(
+                                       "Unexpected error while creating Result Folder node.", re);
+               }
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.slc.jcr/src/org/argeo/slc/jcr/SlcJcrUtils.java b/org.argeo.slc.jcr/src/org/argeo/slc/jcr/SlcJcrUtils.java
new file mode 100644 (file)
index 0000000..9834bef
--- /dev/null
@@ -0,0 +1,253 @@
+package org.argeo.slc.jcr;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.DefaultNameVersion;
+import org.argeo.slc.NameVersion;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.argeo.slc.deploy.ModuleDescriptor;
+import org.argeo.slc.primitive.PrimitiveAccessor;
+import org.argeo.slc.primitive.PrimitiveUtils;
+import org.argeo.slc.test.TestStatus;
+
+/**
+ * Utilities around the SLC JCR model. Note that it relies on fixed base paths
+ * (convention over configuration) for optimization purposes.
+ */
+public class SlcJcrUtils implements SlcNames {
+       public final static Integer AGENT_FACTORY_DEPTH = 3;
+
+       /** Extracts the path of a flow relative to its execution module */
+       public static String flowRelativePath(String fullFlowPath) {
+               String[] tokens = fullFlowPath.split("/");
+               StringBuffer buf = new StringBuffer(fullFlowPath.length());
+               for (int i = AGENT_FACTORY_DEPTH + 3; i < tokens.length; i++) {
+                       buf.append('/').append(tokens[i]);
+               }
+               return buf.toString();
+       }
+
+       /** Extracts the path to the related execution module */
+       public static String modulePath(String fullFlowPath) {
+               String[] tokens = fullFlowPath.split("/");
+               StringBuffer buf = new StringBuffer(fullFlowPath.length());
+               for (int i = 0; i < AGENT_FACTORY_DEPTH + 3; i++) {
+                       if (!tokens[i].equals(""))
+                               buf.append('/').append(tokens[i]);
+               }
+               return buf.toString();
+       }
+
+       /** Extracts the module name from a flow path */
+       public static String moduleName(String fullFlowPath) {
+               String[] tokens = fullFlowPath.split("/");
+               String moduleName = tokens[AGENT_FACTORY_DEPTH + 2];
+               moduleName = moduleName.substring(0, moduleName.indexOf('_'));
+               return moduleName;
+       }
+
+       /** Extracts the module name and version from a flow path */
+       public static NameVersion moduleNameVersion(String fullFlowPath) {
+               String[] tokens = fullFlowPath.split("/");
+               String module = tokens[AGENT_FACTORY_DEPTH + 2];
+               String moduleName = module.substring(0, module.indexOf('_'));
+               String moduleVersion = module.substring(module.indexOf('_') + 1);
+               return new DefaultNameVersion(moduleName, moduleVersion);
+       }
+
+       /** Module node name based on module name and version */
+       public static String getModuleNodeName(ModuleDescriptor moduleDescriptor) {
+               return moduleDescriptor.getName() + "_" + moduleDescriptor.getVersion();
+       }
+
+       /** Extracts the agent factory of a flow */
+       public static String flowAgentFactoryPath(String fullFlowPath) {
+               String[] tokens = fullFlowPath.split("/");
+               StringBuffer buf = new StringBuffer(fullFlowPath.length());
+               // first token is always empty
+               for (int i = 1; i < AGENT_FACTORY_DEPTH + 1; i++) {
+                       buf.append('/').append(tokens[i]);
+               }
+               return buf.toString();
+       }
+
+       /** Create a new execution process path based on the current time */
+       public static String createExecutionProcessPath(Session session, String uuid) {
+               Calendar now = new GregorianCalendar();
+               return getSlcProcessesBasePath(session) + '/'
+                               + JcrUtils.dateAsPath(now, true) + uuid;
+       }
+
+       /** Get the base for the user processi. */
+       public static String getSlcProcessesBasePath(Session session) {
+               try {
+                       Node userHome = CmsJcrUtils.getUserHome(session);
+                       if (userHome == null)
+                               throw new SlcException("No user home available for "
+                                               + session.getUserID());
+                       return userHome.getPath() + '/' + SlcNames.SLC_SYSTEM + '/'
+                                       + SlcNames.SLC_PROCESSES;
+               } catch (RepositoryException re) {
+                       throw new SlcException(
+                                       "Unexpected error while getting Slc Results Base Path.", re);
+               }
+       }
+
+       /**
+        * Create a new execution result path in the user home based on the current
+        * time
+        */
+       public static String createResultPath(Session session, String uuid)
+                       throws RepositoryException {
+               Calendar now = new GregorianCalendar();
+               StringBuffer absPath = new StringBuffer(
+                               SlcJcrResultUtils.getSlcResultsBasePath(session) + '/');
+               // Remove hours and add title property to the result process path on
+               // request of O. Capillon
+               // return getSlcProcessesBasePath(session) + '/'
+               // + JcrUtils.dateAsPath(now, true) + uuid;
+               String relPath = JcrUtils.dateAsPath(now, false);
+               List<String> names = JcrUtils.tokenize(relPath);
+               for (String name : names) {
+                       absPath.append(name + "/");
+                       Node node = JcrUtils.mkdirs(session, absPath.toString());
+                       try {
+                               node.addMixin(NodeType.MIX_TITLE);
+                               node.setProperty(Property.JCR_TITLE, name.substring(1));
+                       } catch (RepositoryException e) {
+                               throw new SlcException(
+                                               "unable to create execution process path", e);
+                       }
+               }
+               return absPath.toString() + uuid;
+       }
+
+       /**
+        * Set the value of the primitive accessor as a JCR property. Does nothing
+        * if the value is null.
+        */
+       public static void setPrimitiveAsProperty(Node node, String propertyName,
+                       PrimitiveAccessor primitiveAccessor) {
+               String type = primitiveAccessor.getType();
+               Object value = primitiveAccessor.getValue();
+               setPrimitiveAsProperty(node, propertyName, type, value);
+       }
+
+       /** Map a primitive value to JCR property value. */
+       public static void setPrimitiveAsProperty(Node node, String propertyName,
+                       String type, Object value) {
+               if (value == null)
+                       return;
+               if (value instanceof CharSequence)
+                       value = PrimitiveUtils.convert(type,
+                                       ((CharSequence) value).toString());
+               if (value instanceof char[])
+                       value = new String((char[]) value);
+
+               try {
+                       if (type.equals(PrimitiveAccessor.TYPE_STRING))
+                               node.setProperty(propertyName, value.toString());
+                       else if (type.equals(PrimitiveAccessor.TYPE_PASSWORD))
+                               node.setProperty(propertyName, value.toString());
+                       else if (type.equals(PrimitiveAccessor.TYPE_INTEGER))
+                               node.setProperty(propertyName, (long) ((Integer) value));
+                       else if (type.equals(PrimitiveAccessor.TYPE_LONG))
+                               node.setProperty(propertyName, ((Long) value));
+                       else if (type.equals(PrimitiveAccessor.TYPE_FLOAT))
+                               node.setProperty(propertyName, (double) ((Float) value));
+                       else if (type.equals(PrimitiveAccessor.TYPE_DOUBLE))
+                               node.setProperty(propertyName, ((Double) value));
+                       else if (type.equals(PrimitiveAccessor.TYPE_BOOLEAN))
+                               node.setProperty(propertyName, ((Boolean) value));
+                       else
+                               throw new SlcException("Unsupported type " + type);
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot set primitive of " + type
+                                       + " as property " + propertyName + " on " + node, e);
+               }
+       }
+
+       /** Aggregates the {@link TestStatus} of this sub-tree. */
+       public static Integer aggregateTestStatus(Node node) {
+               try {
+                       Integer status = TestStatus.PASSED;
+                       if (node.isNodeType(SlcTypes.SLC_CHECK))
+                               if (node.getProperty(SLC_SUCCESS).getBoolean())
+                                       status = TestStatus.PASSED;
+                               else if (node.hasProperty(SLC_ERROR_MESSAGE))
+                                       status = TestStatus.ERROR;
+                               else
+                                       status = TestStatus.FAILED;
+
+                       NodeIterator it = node.getNodes();
+                       while (it.hasNext()) {
+                               Node curr = it.nextNode();
+
+                               // Manually skip aggregated status
+                               if (!SlcNames.SLC_AGGREGATED_STATUS.equals(curr.getName())) {
+                                       Integer childStatus = aggregateTestStatus(curr);
+                                       if (childStatus > status)
+                                               status = childStatus;
+                               }
+                       }
+                       return status;
+               } catch (Exception e) {
+                       throw new SlcException("Could not aggregate test status from "
+                                       + node, e);
+               }
+       }
+
+       /**
+        * Aggregates the {@link TestStatus} of this sub-tree.
+        * 
+        * @return the same {@link StringBuffer}, for convenience (typically calling
+        *         toString() on it)
+        */
+       public static StringBuffer aggregateTestMessages(Node node,
+                       StringBuffer messages) {
+               try {
+                       if (node.isNodeType(SlcTypes.SLC_CHECK)) {
+                               if (node.hasProperty(SLC_MESSAGE)) {
+                                       if (messages.length() > 0)
+                                               messages.append('\n');
+                                       messages.append(node.getProperty(SLC_MESSAGE).getString());
+                               }
+                               if (node.hasProperty(SLC_ERROR_MESSAGE)) {
+                                       if (messages.length() > 0)
+                                               messages.append('\n');
+                                       messages.append(node.getProperty(SLC_ERROR_MESSAGE)
+                                                       .getString());
+                               }
+                       }
+                       NodeIterator it = node.getNodes();
+                       while (it.hasNext()) {
+                               Node child = it.nextNode();
+                               // Manually skip aggregated status
+                               if (!SlcNames.SLC_AGGREGATED_STATUS.equals(child.getName())) {
+                                       aggregateTestMessages(child, messages);
+                               }
+                       }
+                       return messages;
+               } catch (Exception e) {
+                       throw new SlcException("Could not aggregate test messages from "
+                                       + node, e);
+               }
+       }
+
+       /** Prevents instantiation */
+       private SlcJcrUtils() {
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.slc.jcr/src/org/argeo/slc/jcr/execution/JcrAgent.java b/org.argeo.slc.jcr/src/org/argeo/slc/jcr/execution/JcrAgent.java
new file mode 100644 (file)
index 0000000..7f776d1
--- /dev/null
@@ -0,0 +1,116 @@
+package org.argeo.slc.jcr.execution;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.UUID;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.security.Privilege;
+
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcConstants;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.argeo.slc.runtime.DefaultAgent;
+import org.argeo.slc.execution.ExecutionModulesManager;
+import org.argeo.slc.execution.ExecutionProcess;
+import org.argeo.slc.jcr.SlcJcrConstants;
+import org.argeo.slc.runtime.ProcessThread;
+
+/** SLC VM agent synchronizing with a JCR repository. */
+public class JcrAgent extends DefaultAgent implements SlcNames {
+       // final static String ROLE_REMOTE = "ROLE_REMOTE";
+       final static String NODE_REPO_URI = "argeo.node.repo.uri";
+
+       private Repository repository;
+
+       private String agentNodeName = "default";
+
+       /*
+        * LIFECYCLE
+        */
+       protected String initAgentUuid() {
+               Session session = null;
+               try {
+                       session = repository.login();
+
+                       String agentFactoryPath = getAgentFactoryPath();
+                       Node vmAgentFactoryNode = JcrUtils.mkdirsSafe(session, agentFactoryPath, SlcTypes.SLC_AGENT_FACTORY);
+                       JcrUtils.addPrivilege(session, SlcJcrConstants.SLC_BASE_PATH, SlcConstants.ROLE_SLC, Privilege.JCR_ALL);
+                       if (!vmAgentFactoryNode.hasNode(agentNodeName)) {
+                               String uuid = UUID.randomUUID().toString();
+                               Node agentNode = vmAgentFactoryNode.addNode(agentNodeName, SlcTypes.SLC_AGENT);
+                               agentNode.setProperty(SLC_UUID, uuid);
+                       }
+                       session.save();
+                       return vmAgentFactoryNode.getNode(agentNodeName).getProperty(SLC_UUID).getString();
+               } catch (RepositoryException e) {
+                       JcrUtils.discardQuietly(session);
+                       throw new SlcException("Cannot find JCR agent UUID", e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+       @Override
+       public void destroy() {
+               super.destroy();
+       }
+
+       /*
+        * SLC AGENT
+        */
+       @Override
+       protected ProcessThread createProcessThread(ThreadGroup processesThreadGroup,
+                       ExecutionModulesManager modulesManager, ExecutionProcess process) {
+               if (process instanceof JcrExecutionProcess)
+                       return new JcrProcessThread(processesThreadGroup, modulesManager, (JcrExecutionProcess) process);
+               else
+                       return super.createProcessThread(processesThreadGroup, modulesManager, process);
+       }
+
+       /*
+        * UTILITIES
+        */
+       public String getNodePath() {
+               return getAgentFactoryPath() + '/' + getAgentNodeName();
+       }
+
+       public String getAgentFactoryPath() {
+               try {
+                       Boolean isRemote = System.getProperty(NODE_REPO_URI) != null;
+                       String agentFactoryPath;
+                       if (isRemote) {
+                               InetAddress localhost = InetAddress.getLocalHost();
+                               agentFactoryPath = SlcJcrConstants.AGENTS_BASE_PATH + "/" + localhost.getCanonicalHostName();
+
+                               if (agentFactoryPath.equals(SlcJcrConstants.VM_AGENT_FACTORY_PATH))
+                                       throw new SlcException("Unsupported hostname " + localhost.getCanonicalHostName());
+                       } else {// local
+                               agentFactoryPath = SlcJcrConstants.VM_AGENT_FACTORY_PATH;
+                       }
+                       return agentFactoryPath;
+               } catch (UnknownHostException e) {
+                       throw new SlcException("Cannot find agent factory base path", e);
+               }
+       }
+
+       /*
+        * BEAN
+        */
+       public String getAgentNodeName() {
+               return agentNodeName;
+       }
+
+       public void setRepository(Repository repository) {
+               this.repository = repository;
+       }
+
+       public void setAgentNodeName(String agentNodeName) {
+               this.agentNodeName = agentNodeName;
+       }
+}
diff --git a/org.argeo.slc.jcr/src/org/argeo/slc/jcr/execution/JcrExecutionModulesListener.java b/org.argeo.slc.jcr/src/org/argeo/slc/jcr/execution/JcrExecutionModulesListener.java
new file mode 100644 (file)
index 0000000..58f3125
--- /dev/null
@@ -0,0 +1,352 @@
+package org.argeo.slc.jcr.execution;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+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.nodetype.NodeType;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.argeo.slc.deploy.ModuleDescriptor;
+import org.argeo.slc.execution.ExecutionFlowDescriptor;
+import org.argeo.slc.execution.ExecutionModuleDescriptor;
+import org.argeo.slc.execution.ExecutionModulesListener;
+import org.argeo.slc.execution.ExecutionModulesManager;
+import org.argeo.slc.execution.ExecutionSpec;
+import org.argeo.slc.execution.ExecutionSpecAttribute;
+import org.argeo.slc.execution.RefSpecAttribute;
+import org.argeo.slc.execution.RefValueChoice;
+import org.argeo.slc.jcr.SlcJcrUtils;
+import org.argeo.slc.primitive.PrimitiveSpecAttribute;
+import org.argeo.slc.primitive.PrimitiveValue;
+
+/**
+ * Synchronizes the local execution runtime with a JCR repository. For the time
+ * being the state is completely reset from one start to another.
+ */
+public class JcrExecutionModulesListener implements ExecutionModulesListener, SlcNames {
+       private final static String SLC_EXECUTION_MODULES_PROPERTY = "slc.executionModules";
+
+       private final static CmsLog log = CmsLog.getLog(JcrExecutionModulesListener.class);
+       private JcrAgent agent;
+
+       private ExecutionModulesManager modulesManager;
+
+       private Repository repository;
+       /**
+        * We don't use a thread bound session because many different threads will call
+        * this critical component and we don't want to login each time. We therefore
+        * rather protect access to this session via synchronized.
+        */
+       private Session session;
+
+       /*
+        * LIFECYCLE
+        */
+       public void init() {
+               try {
+                       session = repository.login();
+                       clearAgent();
+                       if (modulesManager != null) {
+                               Node agentNode = session.getNode(agent.getNodePath());
+
+                               List<ModuleDescriptor> moduleDescriptors = modulesManager.listModules();
+
+                               // scan SLC-ExecutionModule metadata
+                               for (ModuleDescriptor md : moduleDescriptors) {
+                                       if (md.getMetadata().containsKey(ExecutionModuleDescriptor.SLC_EXECUTION_MODULE)) {
+                                               String moduleNodeName = SlcJcrUtils.getModuleNodeName(md);
+                                               Node moduleNode = agentNode.hasNode(moduleNodeName) ? agentNode.getNode(moduleNodeName)
+                                                               : agentNode.addNode(moduleNodeName);
+                                               moduleNode.addMixin(SlcTypes.SLC_EXECUTION_MODULE);
+                                               moduleNode.setProperty(SLC_NAME, md.getName());
+                                               moduleNode.setProperty(SLC_VERSION, md.getVersion());
+                                               moduleNode.setProperty(Property.JCR_TITLE, md.getTitle());
+                                               moduleNode.setProperty(Property.JCR_DESCRIPTION, md.getDescription());
+                                               moduleNode.setProperty(SLC_STARTED, md.getStarted());
+                                       }
+                               }
+
+                               // scan execution modules property
+                               String executionModules = System.getProperty(SLC_EXECUTION_MODULES_PROPERTY);
+                               if (executionModules != null) {
+                                       for (String executionModule : executionModules.split(",")) {
+                                               allModules: for (ModuleDescriptor md : moduleDescriptors) {
+                                                       String moduleNodeName = SlcJcrUtils.getModuleNodeName(md);
+                                                       if (md.getName().equals(executionModule)) {
+                                                               Node moduleNode = agentNode.hasNode(moduleNodeName) ? agentNode.getNode(moduleNodeName)
+                                                                               : agentNode.addNode(moduleNodeName);
+                                                               moduleNode.addMixin(SlcTypes.SLC_EXECUTION_MODULE);
+                                                               moduleNode.setProperty(SLC_NAME, md.getName());
+                                                               moduleNode.setProperty(SLC_VERSION, md.getVersion());
+                                                               moduleNode.setProperty(Property.JCR_TITLE, md.getTitle());
+                                                               moduleNode.setProperty(Property.JCR_DESCRIPTION, md.getDescription());
+                                                               moduleNode.setProperty(SLC_STARTED, md.getStarted());
+                                                               break allModules;
+                                                       }
+                                               }
+                                       }
+
+                                       // save if needed
+                                       if (session.hasPendingChanges())
+                                               session.save();
+                               }
+                       }
+               } catch (RepositoryException e) {
+                       JcrUtils.discardQuietly(session);
+                       JcrUtils.logoutQuietly(session);
+                       throw new SlcException("Cannot initialize modules", e);
+               }
+       }
+
+       public void destroy() {
+               clearAgent();
+               JcrUtils.logoutQuietly(session);
+       }
+
+       protected synchronized void clearAgent() {
+               try {
+                       Node agentNode = session.getNode(agent.getNodePath());
+                       for (NodeIterator nit = agentNode.getNodes(); nit.hasNext();)
+                               nit.nextNode().remove();
+                       session.save();
+               } catch (RepositoryException e) {
+                       JcrUtils.discardQuietly(session);
+                       throw new SlcException("Cannot clear agent " + agent, e);
+               }
+       }
+
+       /*
+        * EXECUTION MODULES LISTENER
+        */
+
+       public synchronized void executionModuleAdded(ModuleDescriptor moduleDescriptor) {
+               syncExecutionModule(moduleDescriptor);
+       }
+
+       protected void syncExecutionModule(ModuleDescriptor moduleDescriptor) {
+               try {
+                       Node agentNode = session.getNode(agent.getNodePath());
+                       String moduleNodeName = SlcJcrUtils.getModuleNodeName(moduleDescriptor);
+                       Node moduleNode = agentNode.hasNode(moduleNodeName) ? agentNode.getNode(moduleNodeName)
+                                       : agentNode.addNode(moduleNodeName);
+                       moduleNode.addMixin(SlcTypes.SLC_EXECUTION_MODULE);
+                       moduleNode.setProperty(SLC_NAME, moduleDescriptor.getName());
+                       moduleNode.setProperty(SLC_VERSION, moduleDescriptor.getVersion());
+                       moduleNode.setProperty(Property.JCR_TITLE, moduleDescriptor.getTitle());
+                       moduleNode.setProperty(Property.JCR_DESCRIPTION, moduleDescriptor.getDescription());
+                       moduleNode.setProperty(SLC_STARTED, moduleDescriptor.getStarted());
+                       session.save();
+               } catch (RepositoryException e) {
+                       JcrUtils.discardQuietly(session);
+                       throw new SlcException("Cannot sync module " + moduleDescriptor, e);
+               }
+       }
+
+       public synchronized void executionModuleRemoved(ModuleDescriptor moduleDescriptor) {
+               try {
+                       String moduleName = SlcJcrUtils.getModuleNodeName(moduleDescriptor);
+                       Node agentNode = session.getNode(agent.getNodePath());
+                       if (agentNode.hasNode(moduleName)) {
+                               Node moduleNode = agentNode.getNode(moduleName);
+                               for (NodeIterator nit = moduleNode.getNodes(); nit.hasNext();) {
+                                       nit.nextNode().remove();
+                               }
+                               moduleNode.setProperty(SLC_STARTED, false);
+                       }
+                       session.save();
+               } catch (RepositoryException e) {
+                       JcrUtils.discardQuietly(session);
+                       throw new SlcException("Cannot remove module " + moduleDescriptor, e);
+               }
+       }
+
+       public synchronized void executionFlowAdded(ModuleDescriptor module, ExecutionFlowDescriptor efd) {
+               try {
+                       Node agentNode = session.getNode(agent.getNodePath());
+                       Node moduleNode = agentNode.getNode(SlcJcrUtils.getModuleNodeName(module));
+                       String relativePath = getExecutionFlowRelativePath(efd);
+                       @SuppressWarnings("unused")
+                       Node flowNode = null;
+                       if (!moduleNode.hasNode(relativePath)) {
+                               flowNode = createExecutionFlowNode(moduleNode, relativePath, efd);
+                               session.save();
+                       } else {
+                               flowNode = moduleNode.getNode(relativePath);
+                       }
+
+                       if (log.isTraceEnabled())
+                               log.trace("Flow " + efd + " added to JCR");
+               } catch (RepositoryException e) {
+                       JcrUtils.discardQuietly(session);
+                       throw new SlcException("Cannot add flow " + efd + " from module " + module, e);
+               }
+
+       }
+
+       protected Node createExecutionFlowNode(Node moduleNode, String relativePath, ExecutionFlowDescriptor efd)
+                       throws RepositoryException {
+               Node flowNode = null;
+               List<String> pathTokens = Arrays.asList(relativePath.split("/"));
+
+               Iterator<String> names = pathTokens.iterator();
+               // create intermediary paths
+               Node currNode = moduleNode;
+               while (names.hasNext()) {
+                       String name = names.next();
+                       if (currNode.hasNode(name))
+                               currNode = currNode.getNode(name);
+                       else {
+                               if (names.hasNext())
+                                       currNode = currNode.addNode(name);
+                               else
+                                       flowNode = currNode.addNode(name, SlcTypes.SLC_EXECUTION_FLOW);
+                       }
+               }
+
+               // name, description
+               flowNode.setProperty(SLC_NAME, efd.getName());
+               String endName = pathTokens.get(pathTokens.size() - 1);
+               flowNode.setProperty(Property.JCR_TITLE, endName);
+               if (efd.getDescription() != null && !efd.getDescription().trim().equals("")) {
+                       flowNode.setProperty(Property.JCR_DESCRIPTION, efd.getDescription());
+               } else {
+                       flowNode.setProperty(Property.JCR_DESCRIPTION, endName);
+               }
+
+               // execution spec
+               ExecutionSpec executionSpec = efd.getExecutionSpec();
+               String esName = executionSpec.getName();
+               if (esName == null || esName.equals(ExecutionSpec.INTERNAL_NAME)
+                               || esName.contains("#")/* automatically generated bean name */) {
+                       // internal spec node
+                       mapExecutionSpec(flowNode, executionSpec);
+               } else {
+                       // reference spec node
+                       Node executionSpecsNode = moduleNode.hasNode(SLC_EXECUTION_SPECS) ? moduleNode.getNode(SLC_EXECUTION_SPECS)
+                                       : moduleNode.addNode(SLC_EXECUTION_SPECS);
+                       Node executionSpecNode = executionSpecsNode.addNode(esName, SlcTypes.SLC_EXECUTION_SPEC);
+                       executionSpecNode.setProperty(SLC_NAME, esName);
+                       executionSpecNode.setProperty(Property.JCR_TITLE, esName);
+                       if (executionSpec.getDescription() != null && !executionSpec.getDescription().trim().equals(""))
+                               executionSpecNode.setProperty(Property.JCR_DESCRIPTION, executionSpec.getDescription());
+                       mapExecutionSpec(executionSpecNode, executionSpec);
+                       flowNode.setProperty(SLC_SPEC, executionSpecNode);
+               }
+
+               // flow values
+               for (String attr : efd.getValues().keySet()) {
+                       ExecutionSpecAttribute esa = executionSpec.getAttributes().get(attr);
+                       if (esa instanceof PrimitiveSpecAttribute) {
+                               PrimitiveSpecAttribute psa = (PrimitiveSpecAttribute) esa;
+                               // if spec reference there will be no node at this stage
+                               Node valueNode = JcrUtils.getOrAdd(flowNode, attr);
+                               valueNode.setProperty(SLC_TYPE, psa.getType());
+                               SlcJcrUtils.setPrimitiveAsProperty(valueNode, SLC_VALUE, (PrimitiveValue) efd.getValues().get(attr));
+                       }
+               }
+
+               return flowNode;
+       }
+
+       /**
+        * Base can be either an execution spec node, or an execution flow node (in case
+        * the execution spec is internal)
+        */
+       protected void mapExecutionSpec(Node baseNode, ExecutionSpec executionSpec) throws RepositoryException {
+               for (String attrName : executionSpec.getAttributes().keySet()) {
+                       ExecutionSpecAttribute esa = executionSpec.getAttributes().get(attrName);
+                       Node attrNode = baseNode.addNode(attrName);
+                       // booleans
+                       attrNode.addMixin(SlcTypes.SLC_EXECUTION_SPEC_ATTRIBUTE);
+                       attrNode.setProperty(SLC_IS_IMMUTABLE, esa.getIsImmutable());
+                       attrNode.setProperty(SLC_IS_CONSTANT, esa.getIsConstant());
+                       attrNode.setProperty(SLC_IS_HIDDEN, esa.getIsHidden());
+
+                       if (esa instanceof PrimitiveSpecAttribute) {
+                               attrNode.addMixin(SlcTypes.SLC_PRIMITIVE_SPEC_ATTRIBUTE);
+                               PrimitiveSpecAttribute psa = (PrimitiveSpecAttribute) esa;
+                               SlcJcrUtils.setPrimitiveAsProperty(attrNode, SLC_VALUE, psa);
+                               attrNode.setProperty(SLC_TYPE, psa.getType());
+                       } else if (esa instanceof RefSpecAttribute) {
+                               attrNode.addMixin(SlcTypes.SLC_REF_SPEC_ATTRIBUTE);
+                               RefSpecAttribute rsa = (RefSpecAttribute) esa;
+                               attrNode.setProperty(SLC_TYPE, rsa.getTargetClassName());
+                               Object value = rsa.getValue();
+                               if (rsa.getChoices() != null) {
+                                       Integer index = null;
+                                       int count = 0;
+                                       for (RefValueChoice choice : rsa.getChoices()) {
+                                               String name = choice.getName();
+                                               if (value != null && name.equals(value.toString()))
+                                                       index = count;
+                                               Node choiceNode = attrNode.addNode(choice.getName());
+                                               choiceNode.addMixin(NodeType.MIX_TITLE);
+                                               choiceNode.setProperty(Property.JCR_TITLE, choice.getName());
+                                               if (choice.getDescription() != null && !choice.getDescription().trim().equals(""))
+                                                       choiceNode.setProperty(Property.JCR_DESCRIPTION, choice.getDescription());
+                                               count++;
+                                       }
+
+                                       if (index != null)
+                                               attrNode.setProperty(SLC_VALUE, index);
+                               }
+                       }
+               }
+       }
+
+       public synchronized void executionFlowRemoved(ModuleDescriptor module, ExecutionFlowDescriptor executionFlow) {
+               try {
+                       Node agentNode = session.getNode(agent.getNodePath());
+                       Node moduleNode = agentNode.getNode(SlcJcrUtils.getModuleNodeName(module));
+                       String relativePath = getExecutionFlowRelativePath(executionFlow);
+                       if (moduleNode.hasNode(relativePath))
+                               moduleNode.getNode(relativePath).remove();
+                       agentNode.getSession().save();
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot remove flow " + executionFlow + " from module " + module, e);
+               }
+       }
+
+       /*
+        * UTILITIES
+        */
+       /** @return the relative path, never starts with '/' */
+       @SuppressWarnings("deprecation")
+       protected String getExecutionFlowRelativePath(ExecutionFlowDescriptor executionFlow) {
+               String relativePath = executionFlow.getPath() == null ? executionFlow.getName()
+                               : executionFlow.getPath() + '/' + executionFlow.getName();
+               // we assume that it is more than one char long
+               if (relativePath.charAt(0) == '/')
+                       relativePath = relativePath.substring(1);
+               // FIXME quick hack to avoid duplicate '/'
+               relativePath = relativePath.replaceAll("//", "/");
+               return relativePath;
+       }
+
+       /*
+        * BEAN
+        */
+       public void setAgent(JcrAgent agent) {
+               this.agent = agent;
+       }
+
+       public void setRepository(Repository repository) {
+               this.repository = repository;
+       }
+
+       public void setModulesManager(ExecutionModulesManager modulesManager) {
+               this.modulesManager = modulesManager;
+       }
+
+}
diff --git a/org.argeo.slc.jcr/src/org/argeo/slc/jcr/execution/JcrExecutionProcess.java b/org.argeo.slc.jcr/src/org/argeo/slc/jcr/execution/JcrExecutionProcess.java
new file mode 100644 (file)
index 0000000..8cc4dbe
--- /dev/null
@@ -0,0 +1,169 @@
+package org.argeo.slc.jcr.execution;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.NameVersion;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.argeo.slc.execution.ExecutionProcess;
+import org.argeo.slc.execution.ExecutionStep;
+import org.argeo.slc.execution.RealizedFlow;
+import org.argeo.slc.jcr.SlcJcrUtils;
+import org.argeo.slc.runtime.ProcessThread;
+
+/** Execution process implementation based on a JCR node. */
+public class JcrExecutionProcess implements ExecutionProcess, SlcNames {
+       private final static CmsLog log = CmsLog.getLog(JcrExecutionProcess.class);
+       private final Node node;
+
+       private Long nextLogLine = 1l;
+
+       public JcrExecutionProcess(Node node) {
+               this.node = node;
+       }
+
+       public synchronized String getUuid() {
+               try {
+                       return node.getProperty(SLC_UUID).getString();
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot get uuid for " + node, e);
+               }
+       }
+
+       public synchronized String getStatus() {
+               try {
+                       return node.getProperty(SLC_STATUS).getString();
+               } catch (RepositoryException e) {
+                       log.error("Cannot get status: " + e);
+                       // we should re-throw exception because this information can
+                       // probably used for monitoring in case there are already unexpected
+                       // exceptions
+                       return UNKOWN;
+               }
+       }
+
+       public synchronized void setStatus(String status) {
+               try {
+                       node.setProperty(SLC_STATUS, status);
+                       // last modified properties needs to be manually updated
+                       // see https://issues.apache.org/jira/browse/JCR-2233
+                       JcrUtils.updateLastModified(node);
+                       node.getSession().save();
+               } catch (RepositoryException e) {
+                       JcrUtils.discardUnderlyingSessionQuietly(node);
+                       // we should re-throw exception because this information can
+                       // probably used for monitoring in case there are already unexpected
+                       // exceptions
+                       log.error("Cannot set status " + status + ": " + e);
+               }
+       }
+
+       /**
+        * Synchronized in order to make sure that there is no concurrent modification
+        * of {@link #nextLogLine}.
+        */
+       public synchronized void addSteps(List<ExecutionStep> steps) {
+               try {
+                       steps: for (ExecutionStep step : steps) {
+                               String type;
+                               if (step.getType().equals(ExecutionStep.TRACE))
+                                       type = SlcTypes.SLC_LOG_TRACE;
+                               else if (step.getType().equals(ExecutionStep.DEBUG))
+                                       type = SlcTypes.SLC_LOG_DEBUG;
+                               else if (step.getType().equals(ExecutionStep.INFO))
+                                       type = SlcTypes.SLC_LOG_INFO;
+                               else if (step.getType().equals(ExecutionStep.WARNING))
+                                       type = SlcTypes.SLC_LOG_WARNING;
+                               else if (step.getType().equals(ExecutionStep.ERROR))
+                                       type = SlcTypes.SLC_LOG_ERROR;
+                               else
+                                       // skip
+                                       continue steps;
+
+                               String relPath = SLC_LOG + '/' + step.getThread().replace('/', '_') + '/'
+                                               + step.getLocation().replace('.', '/');
+                               String path = node.getPath() + '/' + relPath;
+                               // clean special character
+                               // TODO factorize in JcrUtils
+                               path = path.replace('@', '_');
+
+                               Node location = JcrUtils.mkdirs(node.getSession(), path);
+                               Node logEntry = location.addNode(Long.toString(nextLogLine), type);
+                               logEntry.setProperty(SLC_MESSAGE, step.getLog());
+                               Calendar calendar = new GregorianCalendar();
+                               calendar.setTime(step.getTimestamp());
+                               logEntry.setProperty(SLC_TIMESTAMP, calendar);
+
+                               // System.out.println("Logged " + logEntry.getPath());
+
+                               nextLogLine++;
+                       }
+
+                       // last modified properties needs to be manually updated
+                       // see https://issues.apache.org/jira/browse/JCR-2233
+                       JcrUtils.updateLastModified(node);
+
+                       node.getSession().save();
+               } catch (Exception e) {
+                       JcrUtils.discardUnderlyingSessionQuietly(node);
+                       e.printStackTrace();
+               }
+       }
+
+       // public Node getNode() {
+       // return node;
+       // }
+
+       public List<RealizedFlow> getRealizedFlows() {
+               try {
+                       List<RealizedFlow> realizedFlows = new ArrayList<RealizedFlow>();
+                       Node rootRealizedFlowNode = node.getNode(SLC_FLOW);
+                       // we just manage one level for the time being
+                       NodeIterator nit = rootRealizedFlowNode.getNodes(SLC_FLOW);
+                       while (nit.hasNext()) {
+                               Node realizedFlowNode = nit.nextNode();
+
+                               if (realizedFlowNode.hasNode(SLC_ADDRESS)) {
+                                       String flowPath = realizedFlowNode.getNode(SLC_ADDRESS).getProperty(Property.JCR_PATH).getString();
+                                       NameVersion moduleNameVersion = SlcJcrUtils.moduleNameVersion(flowPath);
+                                       ((ProcessThread) Thread.currentThread()).getExecutionModulesManager().start(moduleNameVersion);
+                               }
+
+                               RealizedFlow realizedFlow = new JcrRealizedFlow(realizedFlowNode);
+                               if (realizedFlow != null)
+                                       realizedFlows.add(realizedFlow);
+                       }
+                       return realizedFlows;
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot get realized flows", e);
+               }
+       }
+
+       public String getNodePath() {
+               try {
+                       return node.getPath();
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot get process node path for " + node, e);
+               }
+       }
+
+       public Repository getRepository() {
+               try {
+                       return node.getSession().getRepository();
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot get process JCR repository for " + node, e);
+               }
+       }
+}
diff --git a/org.argeo.slc.jcr/src/org/argeo/slc/jcr/execution/JcrProcessThread.java b/org.argeo.slc.jcr/src/org/argeo/slc/jcr/execution/JcrProcessThread.java
new file mode 100644 (file)
index 0000000..67c213d
--- /dev/null
@@ -0,0 +1,73 @@
+package org.argeo.slc.jcr.execution;
+
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.execution.ExecutionModulesManager;
+import org.argeo.slc.execution.ExecutionProcess;
+import org.argeo.slc.execution.RealizedFlow;
+import org.argeo.slc.runtime.ProcessThread;
+
+/** Where the actual execution takes place */
+public class JcrProcessThread extends ProcessThread implements SlcNames {
+
+       public JcrProcessThread(ThreadGroup processesThreadGroup, ExecutionModulesManager executionModulesManager,
+                       JcrExecutionProcess process) {
+               super(processesThreadGroup, executionModulesManager, process);
+       }
+
+       /** Overridden in order to set progress status on realized flow nodes. */
+       @Override
+       protected void process() throws InterruptedException {
+               Session session = null;
+               if (getProcess() instanceof JcrExecutionProcess)
+                       try {
+                               session = ((JcrExecutionProcess) getProcess()).getRepository().login(CmsConstants.HOME_WORKSPACE);
+
+                               List<RealizedFlow> realizedFlows = getProcess().getRealizedFlows();
+                               for (RealizedFlow realizedFlow : realizedFlows) {
+                                       Node realizedFlowNode = session.getNode(((JcrRealizedFlow) realizedFlow).getPath());
+                                       setFlowStatus(realizedFlowNode, ExecutionProcess.RUNNING);
+
+                                       try {
+                                               //
+                                               // EXECUTE THE FLOW
+                                               //
+                                               execute(realizedFlow, true);
+
+                                               setFlowStatus(realizedFlowNode, ExecutionProcess.COMPLETED);
+                                       } catch (RepositoryException e) {
+                                               throw e;
+                                       } catch (InterruptedException e) {
+                                               setFlowStatus(realizedFlowNode, ExecutionProcess.KILLED);
+                                               throw e;
+                                       } catch (RuntimeException e) {
+                                               setFlowStatus(realizedFlowNode, ExecutionProcess.ERROR);
+                                               throw e;
+                                       }
+                               }
+                       } catch (RepositoryException e) {
+                               throw new SlcException("Cannot process " + getJcrExecutionProcess().getNodePath(), e);
+                       } finally {
+                               JcrUtils.logoutQuietly(session);
+                       }
+               else
+                       super.process();
+       }
+
+       protected void setFlowStatus(Node realizedFlowNode, String status) throws RepositoryException {
+               realizedFlowNode.setProperty(SLC_STATUS, status);
+               realizedFlowNode.getSession().save();
+       }
+
+       protected JcrExecutionProcess getJcrExecutionProcess() {
+               return (JcrExecutionProcess) getProcess();
+       }
+}
diff --git a/org.argeo.slc.jcr/src/org/argeo/slc/jcr/execution/JcrRealizedFlow.java b/org.argeo.slc.jcr/src/org/argeo/slc/jcr/execution/JcrRealizedFlow.java
new file mode 100644 (file)
index 0000000..b7444d4
--- /dev/null
@@ -0,0 +1,133 @@
+package org.argeo.slc.jcr.execution;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.argeo.slc.execution.ExecutionFlowDescriptor;
+import org.argeo.slc.execution.ExecutionSpecAttribute;
+import org.argeo.slc.execution.RealizedFlow;
+import org.argeo.slc.execution.RefSpecAttribute;
+import org.argeo.slc.jcr.SlcJcrUtils;
+import org.argeo.slc.primitive.PrimitiveSpecAttribute;
+import org.argeo.slc.primitive.PrimitiveUtils;
+import org.argeo.slc.runtime.DefaultExecutionSpec;
+
+public class JcrRealizedFlow extends RealizedFlow implements SlcNames {
+       private static final long serialVersionUID = -3709453850260712001L;
+       private String path;
+
+       public JcrRealizedFlow(Node node) {
+               try {
+                       this.path = node.getPath();
+                       loadFromNode(node);
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot initialize from " + node, e);
+               }
+       }
+
+       protected void loadFromNode(Node realizedFlowNode) throws RepositoryException {
+               if (realizedFlowNode.hasNode(SLC_ADDRESS)) {
+                       String flowPath = realizedFlowNode.getNode(SLC_ADDRESS).getProperty(Property.JCR_PATH).getString();
+                       // TODO: convert to local path if remote
+                       // FIXME start related module
+                       Session agentSession = realizedFlowNode.getSession().getRepository().login();
+                       try {
+                               Node flowNode = agentSession.getNode(flowPath);
+                               String flowName = flowNode.getProperty(SLC_NAME).getString();
+                               String description = null;
+                               if (flowNode.hasProperty(Property.JCR_DESCRIPTION))
+                                       description = flowNode.getProperty(Property.JCR_DESCRIPTION).getString();
+
+                               Node executionModuleNode = flowNode.getSession().getNode(SlcJcrUtils.modulePath(flowPath));
+                               String executionModuleName = executionModuleNode.getProperty(SLC_NAME).getString();
+                               String executionModuleVersion = executionModuleNode.getProperty(SLC_VERSION).getString();
+
+                               RealizedFlow realizedFlow = this;
+                               realizedFlow.setModuleName(executionModuleName);
+                               realizedFlow.setModuleVersion(executionModuleVersion);
+
+                               // retrieve execution spec
+                               DefaultExecutionSpec executionSpec = new DefaultExecutionSpec();
+                               Map<String, ExecutionSpecAttribute> attrs = readExecutionSpecAttributes(realizedFlowNode);
+                               executionSpec.setAttributes(attrs);
+
+                               // set execution spec name
+                               if (flowNode.hasProperty(SlcNames.SLC_SPEC)) {
+                                       Node executionSpecNode = flowNode.getProperty(SLC_SPEC).getNode();
+                                       executionSpec.setName(executionSpecNode.getProperty(SLC_NAME).getString());
+                               }
+
+                               // explicitly retrieve values
+                               Map<String, Object> values = new HashMap<String, Object>();
+                               for (String attrName : attrs.keySet()) {
+                                       ExecutionSpecAttribute attr = attrs.get(attrName);
+                                       Object value = attr.getValue();
+                                       values.put(attrName, value);
+                               }
+
+                               ExecutionFlowDescriptor efd = new ExecutionFlowDescriptor(flowName, description, values, executionSpec);
+                               realizedFlow.setFlowDescriptor(efd);
+
+                       } finally {
+                               JcrUtils.logoutQuietly(agentSession);
+                       }
+               } else {
+                       throw new SlcException("Unsupported realized flow " + realizedFlowNode);
+               }
+       }
+
+       protected Map<String, ExecutionSpecAttribute> readExecutionSpecAttributes(Node node) {
+               try {
+                       Map<String, ExecutionSpecAttribute> attrs = new HashMap<String, ExecutionSpecAttribute>();
+                       for (NodeIterator nit = node.getNodes(); nit.hasNext();) {
+                               Node specAttrNode = nit.nextNode();
+                               if (specAttrNode.isNodeType(SlcTypes.SLC_PRIMITIVE_SPEC_ATTRIBUTE)) {
+                                       String type = specAttrNode.getProperty(SLC_TYPE).getString();
+                                       Object value = null;
+                                       if (specAttrNode.hasProperty(SLC_VALUE)) {
+                                               String valueStr = specAttrNode.getProperty(SLC_VALUE).getString();
+                                               value = PrimitiveUtils.convert(type, valueStr);
+                                       }
+                                       PrimitiveSpecAttribute specAttr = new PrimitiveSpecAttribute(type, value);
+                                       attrs.put(specAttrNode.getName(), specAttr);
+                               } else if (specAttrNode.isNodeType(SlcTypes.SLC_REF_SPEC_ATTRIBUTE)) {
+                                       if (!specAttrNode.hasProperty(SLC_VALUE)) {
+                                               continue;
+                                       }
+                                       Integer value = (int) specAttrNode.getProperty(SLC_VALUE).getLong();
+                                       RefSpecAttribute specAttr = new RefSpecAttribute();
+                                       NodeIterator children = specAttrNode.getNodes();
+                                       int index = 0;
+                                       String id = null;
+                                       while (children.hasNext()) {
+                                               Node child = children.nextNode();
+                                               if (index == value)
+                                                       id = child.getName();
+                                               index++;
+                                       }
+                                       specAttr.setValue(id);
+                                       attrs.put(specAttrNode.getName(), specAttr);
+                               }
+                               // throw new SlcException("Unsupported spec attribute "
+                               // + specAttrNode);
+                       }
+                       return attrs;
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot read spec attributes from " + node, e);
+               }
+       }
+
+       public String getPath() {
+               return path;
+       }
+}
diff --git a/org.argeo.slc.repo/.classpath b/org.argeo.slc.repo/.classpath
new file mode 100644 (file)
index 0000000..b307555
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="src" output="target/classes" path="src"/>
+       <classpathentry kind="src" path="ext/test"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.slc.repo/.gitignore b/org.argeo.slc.repo/.gitignore
new file mode 100644 (file)
index 0000000..0f63015
--- /dev/null
@@ -0,0 +1,2 @@
+/target/
+/bin/
diff --git a/org.argeo.slc.repo/.project b/org.argeo.slc.repo/.project
new file mode 100644 (file)
index 0000000..26f4658
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.slc.repo</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+               <nature>org.eclipse.pde.PluginNature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.slc.repo/META-INF/.gitignore b/org.argeo.slc.repo/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.slc.repo/bnd.bnd b/org.argeo.slc.repo/bnd.bnd
new file mode 100644 (file)
index 0000000..2314c5b
--- /dev/null
@@ -0,0 +1,13 @@
+Import-Package: org.w3c.dom.*,\
+org.xml.sax.*,\
+javax.xml.transform.*,\
+javax.xml.parsers.*,\
+javax.jcr.nodetype,\
+org.osgi.*;version=0.0.0,\
+*
+
+Require-Capability: cms.datamodel; filter:="(name=slc)"
+Provide-Capability: cms.datamodel; name=java,\
+ cms.datamodel; name=dist,\
+ cms.datamodel; name=docs,\
+ cms.datamodel; name=rpm
diff --git a/org.argeo.slc.repo/build.properties b/org.argeo.slc.repo/build.properties
new file mode 100644 (file)
index 0000000..31b02d2
--- /dev/null
@@ -0,0 +1 @@
+additional.bundles = org.junit
diff --git a/org.argeo.slc.repo/ext/test/org/argeo/slc/repo/internal/AetherUtilsTest.java b/org.argeo.slc.repo/ext/test/org/argeo/slc/repo/internal/AetherUtilsTest.java
new file mode 100644 (file)
index 0000000..21563fe
--- /dev/null
@@ -0,0 +1,54 @@
+package org.argeo.slc.repo.internal;
+
+
+import junit.framework.TestCase;
+
+import org.argeo.slc.repo.maven.AetherUtils;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+
+public class AetherUtilsTest extends TestCase {
+       public void testConvertPathToArtifact() throws Exception {
+               checkPathConversion("my.group.id:my-artifactId:pom:1.2.3",
+                               "/my/group/id/my-artifactId/1.2.3/my-artifactId-1.2.3.pom");
+               checkPathConversion("my.group.id:my-artifactId:pom:1.2.3-SNAPSHOT",
+                               "/my/group/id/my-artifactId/1.2.3-SNAPSHOT/my-artifactId-1.2.3-SNAPSHOT.pom");
+               checkPathConversion("my.group.id:my-artifactId:pom:myClassifier:1.2.3",
+                               "/my/group/id/my-artifactId/1.2.3/my-artifactId-1.2.3-myClassifier.pom");
+               checkPathConversion(
+                               "my.group.id:my-artifactId:pom:myClassifier:1.2.3-SNAPSHOT",
+                               "/my/group/id/my-artifactId/1.2.3-SNAPSHOT/my-artifactId-1.2.3-SNAPSHOT-myClassifier.pom");
+               checkPathConversion(
+                               "my.group.id:my-artifactId:pom:myClassifier:20110828.223836-2",
+                               "/my/group/id/my-artifactId/1.2.3-SNAPSHOT/my-artifactId-20110828.223836-2-myClassifier.pom");
+       }
+
+       public void testConvertPathToArtifactRealLife() throws Exception {
+               checkPathConversion(
+                               "org.apache.maven.plugins:maven-antrun-plugin:pom:1.1",
+                               "org/apache/maven/plugins/maven-antrun-plugin/1.1/maven-antrun-plugin-1.1.pom");
+               checkPathConversion(
+                               "org.apache.maven.plugins:maven-plugin-parent:pom:2.0.1",
+                               "org/apache/maven/plugins/maven-plugin-parent/2.0.1/maven-plugin-parent-2.0.1.pom");
+               checkPathConversion(
+                               "org.apache.avalon.framework:avalon-framework-impl:pom:4.3.1",
+                               "org/apache/avalon/framework/avalon-framework-impl/4.3.1/avalon-framework-impl-4.3.1.pom");
+               checkPathConversion(
+                               "org.apache.maven.shared:maven-dependency-tree:pom:1.2",
+                               "org/apache/maven/shared/maven-dependency-tree/1.2/maven-dependency-tree-1.2.pom");
+               checkPathConversion(
+                               "org.argeo.maven.plugins:maven-argeo-osgi-plugin:pom:1.0.33",
+                               "org/argeo/maven/plugins/maven-argeo-osgi-plugin/1.0.33/maven-argeo-osgi-plugin-1.0.33.pom");
+               checkPathConversion(
+                               "org.apache.maven.plugins:maven-clean-plugin:pom:2.4.1",
+                               "org/apache/maven/plugins/maven-clean-plugin/2.4.1/maven-clean-plugin-2.4.1.pom");
+       }
+
+       protected void checkPathConversion(String expectedArtifact, String path) {
+               Artifact artifact = AetherUtils.convertPathToArtifact(path, null);
+               if (expectedArtifact == null)
+                       assertNull(artifact);
+               else
+                       assertEquals(new DefaultArtifact(expectedArtifact), artifact);
+       }
+}
diff --git a/org.argeo.slc.repo/ext/test/org/argeo/slc/repo/internal/pom.xml b/org.argeo.slc.repo/ext/test/org/argeo/slc/repo/internal/pom.xml
new file mode 100644 (file)
index 0000000..b12659b
--- /dev/null
@@ -0,0 +1,209 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.70-SNAPSHOT</version>
+       </parent>
+       <groupId>org.argeo.slc</groupId>
+       <artifactId>argeo-slc</artifactId>
+       <packaging>pom</packaging>
+       <name>Argeo SLC</name>
+       <version>2.1.10-SNAPSHOT</version>
+       <properties>
+               <developmentCycle.slc>2.1</developmentCycle.slc>
+               <developmentCycle.startDate>2015-02-12</developmentCycle.startDate>
+               <version.argeo-rcp>2.1.15</version.argeo-rcp>
+               <version.slc>2.1.10-SNAPSHOT</version.slc>
+               <version.equinox>3.11.1.v20160708-1632</version.equinox>
+               <!-- Embedded Maven -->
+               <version.maven>3.2.5</version.maven>
+       </properties>
+       <modules>
+               <!-- Runtime -->
+               <module>org.argeo.slc.api</module>
+               <module>org.argeo.slc.core</module>
+               <module>org.argeo.slc.unit</module>
+               <module>org.argeo.slc.support</module>
+               <module>org.argeo.slc.support.maven</module>
+               <module>org.argeo.slc.repo</module>
+               <module>org.argeo.slc.factory</module>
+               <module>org.argeo.slc.launcher</module>
+
+               <!-- Modules -->
+               <module>org.argeo.slc.agent</module>
+               <module>org.argeo.slc.agent.jcr</module>
+               <module>org.argeo.slc.server.repo</module>
+
+               <!-- UI -->
+               <module>org.argeo.slc.client.ui</module>
+               <module>org.argeo.slc.client.ui.dist</module>
+               <module>org.argeo.slc.client.rap</module>
+               <!-- <module>org.argeo.slc.client.rcp</module> -->
+
+               <module>lib</module>
+               <module>dep</module>
+               <module>dist</module>
+               <module>demo</module>
+       </modules>
+       <url>http://projects.argeo.org/slc/</url>
+       <scm>
+               <connection>scm:git:http://git.argeo.org/apache2/argeo-slc.git</connection>
+               <url>http://git.argeo.org/?p=apache2/argeo-slc.git;a=summary</url>
+               <developerConnection>scm:git:https://code.argeo.org/git/apache2/argeo-slc.git</developerConnection>
+               <tag>HEAD</tag>
+       </scm>
+       <inceptionYear>2007</inceptionYear>
+       <licenses>
+               <license>
+                       <name>Apache 2</name>
+                       <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+                       <distribution>repo</distribution>
+                       <comments><![CDATA[
+SLC (Software Life Cycle) framework
+                          
+Copyright (C) 2007-2012 Argeo GmbH
+
+Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+]]>
+                       </comments>
+               </license>
+       </licenses>
+       <developers>
+               <developer>
+                       <id>mbaudier</id>
+                       <name>Mathieu Baudier</name>
+                       <email><![CDATA[http://mailhide.recaptcha.net/d?k=01EM7GpnvY3k8woQ2tnnZLUA==&c=crsNpHjhOBDPswHG6HD_gXaqymhC69wmBf7wlagcSHw=]]></email>
+                       <organization>Argeo</organization>
+                       <organizationUrl>http://www.argeo.org</organizationUrl>
+                       <roles>
+                               <role>architect</role>
+                               <role>developer</role>
+                               <role>QA</role>
+                       </roles>
+               </developer>
+               <developer>
+                       <id>ocapillo</id>
+                       <name>Olivier Capillon</name>
+                       <email><![CDATA[http://mailhide.recaptcha.net/d?k=01EM7GpnvY3k8woQ2tnnZLUA==&c=BYw8i94WiejnvegUKJoCZQQr0h-mYlKCNKZVe_3WPIA=]]></email>
+                       <organization>Argeo</organization>
+                       <organizationUrl>http://www.argeo.org</organizationUrl>
+                       <roles>
+                               <role>developer</role>
+                       </roles>
+               </developer>
+               <developer>
+                       <id>bsinou</id>
+                       <name>Bruno Sinou</name>
+                       <email><![CDATA[http://www.google.com/recaptcha/mailhide/d?k=01SZoYvDnJzcw0KOR7M7u6Qg==&c=SVgEjXA_Uu9ZrNzLES92w1ght6puLFiVpoNUddCfSU8=]]></email>
+                       <organization>Argeo</organization>
+                       <organizationUrl>http://www.argeo.org</organizationUrl>
+                       <roles>
+                               <role>developer</role>
+                       </roles>
+               </developer>
+       </developers>
+       <build>
+               <plugins>
+                       <plugin>
+                               <artifactId>maven-site-plugin</artifactId>
+                               <inherited>false</inherited>
+                               <configuration>
+                                       <skip>false</skip>
+                               </configuration>
+                       </plugin>
+                       <plugin>
+                               <artifactId>maven-javadoc-plugin</artifactId>
+                               <configuration>
+                                       <skip>true</skip>
+                               </configuration>
+                       </plugin>
+               </plugins>
+       </build>
+       <repositories>
+               <repository>
+                       <id>argeo</id>
+                       <url>http://forge.argeo.org/data/java/argeo-2.1/</url>
+                       <releases>
+                               <enabled>true</enabled>
+                               <updatePolicy>daily</updatePolicy>
+                               <checksumPolicy>warn</checksumPolicy>
+                       </releases>
+               </repository>
+               <repository>
+                       <id>argeo-rcp</id>
+                       <url>http://forge.argeo.org/data/java/argeo-rcp-2.1</url>
+                       <releases>
+                               <enabled>true</enabled>
+                               <updatePolicy>daily</updatePolicy>
+                               <checksumPolicy>warn</checksumPolicy>
+                       </releases>
+               </repository>
+
+               <!-- Disable Maven default repository -->
+               <repository>
+                       <id>central</id>
+                       <url>http://repo1.maven.org/maven2</url>
+                       <releases>
+                               <enabled>false</enabled>
+                       </releases>
+                       <snapshots>
+                               <enabled>false</enabled>
+                       </snapshots>
+               </repository>
+       </repositories>
+       <profiles>
+               <profile>
+                       <id>localrepo</id>
+                       <repositories>
+                               <repository>
+                                       <id>argeo-tp</id>
+                                       <url>http://localhost:7070/data/java/argeo-${developmentCycle.argeo-commons}</url>
+                                       <releases>
+                                               <enabled>true</enabled>
+                                               <updatePolicy>daily</updatePolicy>
+                                               <checksumPolicy>warn</checksumPolicy>
+                                       </releases>
+                               </repository>
+                               <!-- <repository> -->
+                               <!-- <id>argeo-tp-extras</id> -->
+                               <!-- <url>http://localhost:7080/data/java/argeo-tp-extras-2.1</url> -->
+                               <!-- <releases> -->
+                               <!-- <enabled>true</enabled> -->
+                               <!-- <updatePolicy>daily</updatePolicy> -->
+                               <!-- <checksumPolicy>warn</checksumPolicy> -->
+                               <!-- </releases> -->
+                               <!-- </repository> -->
+                               <repository>
+                                       <id>argeo-commons</id>
+                                       <url>http://localhost:7070/data/java/argeo-${developmentCycle.argeo-commons}</url>
+                                       <releases>
+                                               <enabled>true</enabled>
+                                               <updatePolicy>daily</updatePolicy>
+                                               <checksumPolicy>warn</checksumPolicy>
+                                       </releases>
+                               </repository>
+                       </repositories>
+                       <distributionManagement>
+                               <repository>
+                                       <id>staging</id>
+                                       <url>dav:http://localhost:7070/data/java/argeo-slc-${developmentCycle.slc}</url>
+                               </repository>
+                               <site>
+                                       <id>staging</id>
+                                       <url>dav:http://localhost:7070/data/docs/argeo-slc-${developmentCycle.slc}</url>
+                               </site>
+                       </distributionManagement>
+               </profile>
+       </profiles>
+</project>
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/ArgeoOsgiDistribution.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/ArgeoOsgiDistribution.java
new file mode 100644 (file)
index 0000000..4b8878a
--- /dev/null
@@ -0,0 +1,11 @@
+package org.argeo.slc.repo;
+
+import org.argeo.slc.CategoryNameVersion;
+import org.argeo.slc.build.Distribution;
+import org.argeo.slc.build.ModularDistribution;
+
+/** Aether compatible OSGi distribution */
+public interface ArgeoOsgiDistribution extends Distribution,
+               CategoryNameVersion, ModularDistribution {
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/ArtifactDistribution.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/ArtifactDistribution.java
new file mode 100644 (file)
index 0000000..bc496f1
--- /dev/null
@@ -0,0 +1,63 @@
+package org.argeo.slc.repo;
+
+import org.argeo.slc.CategoryNameVersion;
+import org.argeo.slc.build.Distribution;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+
+/** A {@link Distribution} based on an Aether {@link Artifact} */
+public class ArtifactDistribution implements Distribution,
+               CategoryNameVersion {
+       private final Artifact artifact;
+
+       public ArtifactDistribution(Artifact artifact) {
+               this.artifact = artifact;
+       }
+
+       public ArtifactDistribution(String coords) {
+               this(new DefaultArtifact(coords));
+       }
+
+       /** Aether coordinates of the underlying artifact. */
+       public String getDistributionId() {
+               return artifact.toString();
+       }
+
+       public Artifact getArtifact() {
+               return artifact;
+       }
+
+       public String getName() {
+               return getArtifact().getArtifactId();
+       }
+
+       public String getVersion() {
+               return getArtifact().getVersion();
+       }
+
+       public String getCategory() {
+               return getArtifact().getGroupId();
+       }
+
+       @Override
+       public int hashCode() {
+               return artifact.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj instanceof CategoryNameVersion) {
+                       CategoryNameVersion cnv = (CategoryNameVersion) obj;
+                       return getCategory().equals(cnv.getCategory())
+                                       && getName().equals(cnv.getName())
+                                       && getVersion().equals(cnv.getVersion());
+               } else
+                       return artifact.equals(obj);
+       }
+
+       @Override
+       public String toString() {
+               return getDistributionId();
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/ArtifactIndexer.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/ArtifactIndexer.java
new file mode 100644 (file)
index 0000000..209f2b6
--- /dev/null
@@ -0,0 +1,217 @@
+package org.argeo.slc.repo;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.argeo.slc.repo.maven.AetherUtils;
+import org.eclipse.aether.artifact.Artifact;
+import org.osgi.framework.Constants;
+
+/**
+ * Add {@link Artifact} properties to a {@link Node}. Does nothing if the node
+ * name doesn't start with the artifact id (in order to skip Maven metadata XML
+ * files and other non artifact files).
+ */
+public class ArtifactIndexer implements NodeIndexer, SlcNames {
+       private CmsLog log = CmsLog.getLog(ArtifactIndexer.class);
+       private Boolean force = false;
+
+       public Boolean support(String path) {
+               String relativePath = getRelativePath(path);
+               if (relativePath == null)
+                       return false;
+               Artifact artifact = null;
+               try {
+                       artifact = AetherUtils.convertPathToArtifact(relativePath, null);
+               } catch (Exception e) {
+                       if (log.isTraceEnabled())
+                               log.trace("Malformed path " + path + ", skipping silently", e);
+               }
+               return artifact != null;
+       }
+
+       public void index(Node fileNode) {
+               Artifact artifact = null;
+               try {
+                       if (!support(fileNode.getPath()))
+                               return;
+
+                       // Already indexed
+                       if (!force && fileNode.isNodeType(SlcTypes.SLC_ARTIFACT))
+                               return;
+
+                       if (!fileNode.isNodeType(NodeType.NT_FILE))
+                               return;
+
+                       String relativePath = getRelativePath(fileNode.getPath());
+                       if (relativePath == null)
+                               return;
+                       artifact = AetherUtils.convertPathToArtifact(relativePath, null);
+                       // support() guarantees that artifact won't be null, no NPE check
+                       fileNode.addMixin(SlcTypes.SLC_ARTIFACT);
+                       fileNode.setProperty(SlcNames.SLC_ARTIFACT_ID, artifact.getArtifactId());
+                       fileNode.setProperty(SlcNames.SLC_GROUP_ID, artifact.getGroupId());
+                       fileNode.setProperty(SlcNames.SLC_ARTIFACT_VERSION, artifact.getVersion());
+                       fileNode.setProperty(SlcNames.SLC_ARTIFACT_EXTENSION, artifact.getExtension());
+                       // can be null but ok for JCR API
+                       fileNode.setProperty(SlcNames.SLC_ARTIFACT_CLASSIFIER, artifact.getClassifier());
+                       JcrUtils.updateLastModified(fileNode);
+
+                       // make sure there are checksums
+                       String shaNodeName = fileNode.getName() + ".sha1";
+                       if (!fileNode.getParent().hasNode(shaNodeName)) {
+                               String sha = JcrUtils.checksumFile(fileNode, "SHA-1");
+                               JcrUtils.copyBytesAsFile(fileNode.getParent(), shaNodeName, sha.getBytes());
+                       }
+                       String md5NodeName = fileNode.getName() + ".md5";
+                       if (!fileNode.getParent().hasNode(md5NodeName)) {
+                               String md5 = JcrUtils.checksumFile(fileNode, "MD5");
+                               JcrUtils.copyBytesAsFile(fileNode.getParent(), md5NodeName, md5.getBytes());
+                       }
+
+                       // Create a default pom if none already exist
+                       String fileNodeName = fileNode.getName();
+                       String pomName = null;
+                       if (fileNodeName.endsWith(".jar"))
+                               pomName = fileNodeName.substring(0, fileNodeName.length() - ".jar".length()) + ".pom";
+
+                       if (pomName != null && !fileNode.getParent().hasNode(pomName)) {
+                               String pom = generatePomForBundle(fileNode);
+                               Node pomNode = JcrUtils.copyBytesAsFile(fileNode.getParent(), pomName, pom.getBytes());
+                               // corresponding check sums
+                               String sha = JcrUtils.checksumFile(pomNode, "SHA-1");
+                               JcrUtils.copyBytesAsFile(fileNode.getParent(), pomName + ".sha1", sha.getBytes());
+                               String md5 = JcrUtils.checksumFile(fileNode, "MD5");
+                               JcrUtils.copyBytesAsFile(fileNode.getParent(), pomName + ".md5", md5.getBytes());
+                       }
+
+                       // set higher levels
+                       Node artifactVersionBase = fileNode.getParent();
+                       if (!artifactVersionBase.isNodeType(SlcTypes.SLC_ARTIFACT_VERSION_BASE)) {
+                               artifactVersionBase.addMixin(SlcTypes.SLC_ARTIFACT_VERSION_BASE);
+                               artifactVersionBase.setProperty(SlcNames.SLC_ARTIFACT_VERSION, artifact.getBaseVersion());
+                               artifactVersionBase.setProperty(SlcNames.SLC_ARTIFACT_ID, artifact.getArtifactId());
+                               artifactVersionBase.setProperty(SlcNames.SLC_GROUP_ID, artifact.getGroupId());
+                       }
+                       JcrUtils.updateLastModified(artifactVersionBase);
+
+                       // pom
+                       if (artifact.getExtension().equals("pom")) {
+                               // TODO read to make it a distribution
+                       }
+
+                       Node artifactBase = artifactVersionBase.getParent();
+                       if (!artifactBase.isNodeType(SlcTypes.SLC_ARTIFACT_BASE)) {
+                               artifactBase.addMixin(SlcTypes.SLC_ARTIFACT_BASE);
+                               artifactBase.setProperty(SlcNames.SLC_ARTIFACT_ID, artifact.getArtifactId());
+                               artifactBase.setProperty(SlcNames.SLC_GROUP_ID, artifact.getGroupId());
+                       }
+                       JcrUtils.updateLastModified(artifactBase);
+
+                       Node groupBase = artifactBase.getParent();
+                       if (!groupBase.isNodeType(SlcTypes.SLC_GROUP_BASE)) {
+                               // if (groupBase.isNodeType(SlcTypes.SLC_ARTIFACT_BASE)) {
+                               // log.warn("Group base " + groupBase.getPath()
+                               // + " is also artifact base");
+                               // }
+                               groupBase.addMixin(SlcTypes.SLC_GROUP_BASE);
+                               groupBase.setProperty(SlcNames.SLC_GROUP_BASE_ID, artifact.getGroupId());
+                       }
+                       JcrUtils.updateLastModifiedAndParents(groupBase, RepoConstants.DEFAULT_ARTIFACTS_BASE_PATH);
+
+                       if (log.isTraceEnabled())
+                               log.trace("Indexed artifact " + artifact + " on " + fileNode);
+               } catch (Exception e) {
+                       throw new SlcException("Cannot index artifact " + artifact + " metadata on node " + fileNode, e);
+               }
+       }
+
+       private String getRelativePath(String nodePath) {
+               String basePath = RepoConstants.DEFAULT_ARTIFACTS_BASE_PATH;
+               if (!nodePath.startsWith(basePath))
+                       return null;
+               String relativePath = nodePath.substring(basePath.length());
+               return relativePath;
+       }
+
+       public void setForce(Boolean force) {
+               this.force = force;
+       }
+
+       private String generatePomForBundle(Node n) throws RepositoryException {
+               StringBuffer p = new StringBuffer();
+               p.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
+               p.append(
+                               "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n");
+               p.append("<modelVersion>4.0.0</modelVersion>");
+
+               // Categorized name version
+               p.append("<groupId>").append(JcrUtils.get(n, SLC_GROUP_ID)).append("</groupId>\n");
+               p.append("<artifactId>").append(JcrUtils.get(n, SLC_ARTIFACT_ID)).append("</artifactId>\n");
+               p.append("<version>").append(JcrUtils.get(n, SLC_ARTIFACT_VERSION)).append("</version>\n");
+               // TODO make it more generic
+               p.append("<packaging>jar</packaging>\n");
+               if (n.hasProperty(SLC_ + Constants.BUNDLE_NAME))
+                       p.append("<name>").append(JcrUtils.get(n, SLC_ + Constants.BUNDLE_NAME)).append("</name>\n");
+               if (n.hasProperty(SLC_ + Constants.BUNDLE_DESCRIPTION))
+                       p.append("<description>").append(JcrUtils.get(n, SLC_ + Constants.BUNDLE_DESCRIPTION))
+                                       .append("</description>\n");
+
+               // Dependencies in case of a distribution
+               if (n.isNodeType(SlcTypes.SLC_MODULAR_DISTRIBUTION)) {
+                       p.append(getDependenciesSnippet(n.getNode(SlcNames.SLC_MODULES).getNodes()));
+                       p.append(getDependencyManagementSnippet(n.getNode(SlcNames.SLC_MODULES).getNodes()));
+               }
+               p.append("</project>\n");
+               return p.toString();
+       }
+
+       private String getDependenciesSnippet(NodeIterator nit) throws RepositoryException {
+               StringBuilder b = new StringBuilder();
+               b.append("<dependencies>\n");
+               while (nit.hasNext()) {
+                       Node currModule = nit.nextNode();
+                       if (currModule.isNodeType(SlcTypes.SLC_MODULE_COORDINATES)) {
+                               b.append(getDependencySnippet(currModule.getProperty(SlcNames.SLC_CATEGORY).getString(),
+                                               currModule.getProperty(SlcNames.SLC_NAME).getString(), null));
+                       }
+               }
+               b.append("</dependencies>\n");
+               return b.toString();
+       }
+
+       private String getDependencyManagementSnippet(NodeIterator nit) throws RepositoryException {
+               StringBuilder b = new StringBuilder();
+               b.append("<dependencyManagement>\n");
+               b.append("<dependencies>\n");
+               while (nit.hasNext()) {
+                       Node currModule = nit.nextNode();
+                       if (currModule.isNodeType(SlcTypes.SLC_MODULE_COORDINATES)) {
+                               b.append(getDependencySnippet(currModule.getProperty(SlcNames.SLC_CATEGORY).getString(),
+                                               currModule.getProperty(SlcNames.SLC_NAME).getString(),
+                                               currModule.getProperty(SlcNames.SLC_VERSION).getString()));
+                       }
+               }
+               b.append("</dependencies>\n");
+               b.append("</dependencyManagement>\n");
+               return b.toString();
+       }
+
+       private String getDependencySnippet(String category, String name, String version) {
+               StringBuilder b = new StringBuilder();
+               b.append("<dependency>\n");
+               b.append("\t<groupId>").append(category).append("</groupId>\n");
+               b.append("\t<artifactId>").append(name).append("</artifactId>\n");
+               if (version != null)
+                       b.append("\t<version>").append(version).append("</version>\n");
+               b.append("</dependency>\n");
+               return b.toString();
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/FreeLicense.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/FreeLicense.java
new file mode 100644 (file)
index 0000000..5cb290c
--- /dev/null
@@ -0,0 +1,161 @@
+package org.argeo.slc.repo;
+
+import java.io.InputStream;
+import java.net.URL;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.build.License;
+
+/** A free software license */
+public abstract class FreeLicense implements License {
+       final static String RESOURCES = "/org/argeo/slc/repo/license/";
+
+       /** GNU */
+       public final static FreeLicense GPL_v3 = new FreeLicense("GPL-3.0-or-later",
+                       "http://www.gnu.org/licenses/gpl-3.0.txt", null, RESOURCES + "gpl-3.0.txt") {
+       };
+
+       public final static FreeLicense GPL_v2 = new FreeLicense("GPL-2.0-or-later",
+                       "http://www.gnu.org/licenses/gpl-2.0.txt", null, RESOURCES + "gpl-2.0.txt") {
+       };
+       public final static FreeLicense GPL = GPL_v3;
+
+       public final static FreeLicense LGPL_v3 = new FreeLicense("LGPL-3.0-or-later",
+                       "http://www.gnu.org/licenses/lgpl-3.0.txt", null, RESOURCES + "lgpl-3.0.txt") {
+       };
+
+       public final static FreeLicense LGPL_v2 = new FreeLicense("LGPL-2.0-or-later",
+                       "http://www.gnu.org/licenses/lgpl-2.1.txt", null, RESOURCES + "lgpl-2.1.txt") {
+       };
+       public final static FreeLicense LGPL = LGPL_v3;
+
+       /** Apache */
+       public final static FreeLicense APACHE_v2 = new FreeLicense("Apache-2.0",
+                       "http://www.apache.org/licenses/LICENSE-2.0.txt", null, RESOURCES + "apache-2.0.txt") {
+       };
+       public final static FreeLicense APACHE = APACHE_v2;
+
+       /** Eclipse */
+       public final static FreeLicense EPL_v1 = new FreeLicense("EPL-1.0", "http://www.eclipse.org/legal/epl-v10.html",
+                       null, RESOURCES + "epl-1.0.txt") {
+       };
+       public final static FreeLicense EPL_v2 = new FreeLicense("EPL-2.0", "http://www.eclipse.org/legal/epl-v20.html",
+                       null, RESOURCES + "epl-1.0.txt") {
+       };
+       public final static FreeLicense EPL = EPL_v1;
+
+       /** Miscellaneous */
+       public final static FreeLicense MIT = new FreeLicense("MIT", "http://opensource.org/licenses/MIT", null,
+                       RESOURCES + "mit.txt") {
+       };
+
+       public final static FreeLicense BSD_NEW = new FreeLicense("BSD-3-Clause",
+                       "http://opensource.org/licenses/BSD-3-Clause", null, RESOURCES + "bsd-3-clause.txt") {
+       };
+
+       public final static FreeLicense BSD = BSD_NEW;
+
+       public final static FreeLicense CDDL_v1 = new FreeLicense("CDDL-1.0", "http://opensource.org/licenses/CDDL-1.0",
+                       null, RESOURCES + "cddl-1.0.txt") {
+       };
+       public final static FreeLicense CDDL = CDDL_v1;
+
+       public final static FreeLicense MOZILLA_v2 = new FreeLicense("MPL-2.0", "https://opensource.org/licenses/MPL-2.0",
+                       null, RESOURCES + "cddl-1.0.txt") {
+       };
+       public final static FreeLicense MOZILLA = MOZILLA_v2;
+
+       /** Public domain corner case */
+       public final static License PUBLIC_DOMAIN = new License() {
+
+               public String getUri() {
+                       return "http://creativecommons.org/about/pdm";
+               }
+
+               public String getText() {
+                       return "This work is free of known copyright restrictions.";
+               }
+
+               public String getName() {
+                       return "Public Domain License";
+               }
+
+               public String getLink() {
+                       return "http://wiki.creativecommons.org/PDM_FAQ";
+               }
+       };
+
+       private final String name, uri, link, resource;
+
+       public FreeLicense(String name, String uri) {
+               this(name, uri, null, null);
+       }
+
+       public FreeLicense(String name, String uri, String link) {
+               this(name, uri, link, null);
+       }
+
+       public FreeLicense(String name, String uri, String link, String resource) {
+               if (uri == null)
+                       throw new SlcException("URI cannot be null");
+               this.name = name;
+               this.uri = uri;
+               this.link = link;
+               this.resource = resource;
+               getText();
+       }
+
+       public String getName() {
+               return name;
+       }
+
+       public String getUri() {
+               return uri;
+       }
+
+       public String getLink() {
+               return link;
+       }
+
+       @Override
+       public String getText() {
+               InputStream in = null;
+               URL url = null;
+               try {
+                       if (resource != null)
+                               url = getClass().getClassLoader().getResource(resource);
+                       else
+                               url = new URL(uri);
+                       in = url.openStream();
+                       String text = IOUtils.toString(in);
+                       return text;
+               } catch (Exception e) {
+                       throw new SlcException("Cannot retrieve license " + name + " from " + url, e);
+               } finally {
+                       IOUtils.closeQuietly(in);
+               }
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (!(obj instanceof License))
+                       return false;
+               return ((License) obj).getUri().equals(getUri());
+       }
+
+       @Override
+       public int hashCode() {
+               return getUri().hashCode();
+       }
+
+       @Override
+       public String toString() {
+               StringBuilder sb = new StringBuilder(name != null ? name : uri);
+//             if (link != null)
+//                     sb.append(';').append("link=").append(link);
+//             else if (uri != null && name != null)
+//                     sb.append(';').append("link=").append(uri);
+               return sb.toString();
+       }
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/JarFileIndexer.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/JarFileIndexer.java
new file mode 100644 (file)
index 0000000..5d4bf5b
--- /dev/null
@@ -0,0 +1,443 @@
+package org.argeo.slc.repo;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+import java.util.jar.Attributes;
+import java.util.jar.Attributes.Name;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+
+/**
+ * Indexes jar file, currently supports standard J2SE and OSGi metadata (both
+ * from MANIFEST)
+ */
+public class JarFileIndexer implements NodeIndexer, SlcNames {
+       private final static CmsLog log = CmsLog.getLog(JarFileIndexer.class);
+       private Boolean force = false;
+
+       public Boolean support(String path) {
+               return FilenameUtils.getExtension(path).equals("jar");
+       }
+
+       public void index(Node fileNode) {
+               Binary fileBinary = null;
+               JarInputStream jarIn = null;
+               ByteArrayOutputStream bo = null;
+               ByteArrayInputStream bi = null;
+               Binary manifestBinary = null;
+               try {
+                       if (!support(fileNode.getPath()))
+                               return;
+
+                       // Already indexed
+                       if (!force && fileNode.isNodeType(SlcTypes.SLC_JAR_FILE))
+                               return;
+
+                       if (!fileNode.isNodeType(NodeType.NT_FILE))
+                               return;
+
+                       Session jcrSession = fileNode.getSession();
+                       Node contentNode = fileNode.getNode(Node.JCR_CONTENT);
+                       fileBinary = contentNode.getProperty(Property.JCR_DATA).getBinary();
+
+                       jarIn = new JarInputStream(fileBinary.getStream());
+                       Manifest manifest = jarIn.getManifest();
+                       if (manifest == null) {
+                               log.error(fileNode + " has no MANIFEST");
+                               return;
+                       }
+
+                       bo = new ByteArrayOutputStream();
+                       manifest.write(bo);
+                       byte[] newManifest = bo.toByteArray();
+                       if (fileNode.hasProperty(SLC_MANIFEST)) {
+                               byte[] storedManifest = JcrUtils.getBinaryAsBytes(fileNode.getProperty(SLC_MANIFEST));
+                               if (Arrays.equals(newManifest, storedManifest)) {
+                                       if (log.isTraceEnabled())
+                                               log.trace("Manifest not changed, doing nothing " + fileNode);
+                                       return;
+                               }
+                       }
+
+                       bi = new ByteArrayInputStream(newManifest);
+                       manifestBinary = jcrSession.getValueFactory().createBinary(bi);
+
+                       // standard jar file
+                       fileNode.addMixin(SlcTypes.SLC_JAR_FILE);
+
+                       fileNode.setProperty(SlcNames.SLC_MANIFEST, manifestBinary);
+                       Attributes attrs = manifest.getMainAttributes();
+
+                       getI18nValues(fileBinary, attrs);
+
+                       // standard J2SE MANIFEST attributes
+                       addAttr(Attributes.Name.MANIFEST_VERSION, fileNode, attrs);
+                       addAttr(Attributes.Name.SIGNATURE_VERSION, fileNode, attrs);
+                       addAttr(Attributes.Name.CLASS_PATH, fileNode, attrs);
+                       addAttr(Attributes.Name.MAIN_CLASS, fileNode, attrs);
+                       addAttr(Attributes.Name.EXTENSION_NAME, fileNode, attrs);
+                       addAttr(Attributes.Name.IMPLEMENTATION_VERSION, fileNode, attrs);
+                       addAttr(Attributes.Name.IMPLEMENTATION_VENDOR, fileNode, attrs);
+                       addAttr(Attributes.Name.IMPLEMENTATION_VENDOR_ID, fileNode, attrs);
+                       addAttr(Attributes.Name.SPECIFICATION_TITLE, fileNode, attrs);
+                       addAttr(Attributes.Name.SPECIFICATION_VERSION, fileNode, attrs);
+                       addAttr(Attributes.Name.SPECIFICATION_VENDOR, fileNode, attrs);
+                       addAttr(Attributes.Name.SEALED, fileNode, attrs);
+
+                       // OSGi
+                       if (attrs.containsKey(new Name(Constants.BUNDLE_SYMBOLICNAME))) {
+                               addOsgiMetadata(fileNode, attrs);
+                               if (log.isTraceEnabled())
+                                       log.trace("Indexed OSGi bundle " + fileNode);
+                       } else {
+                               if (log.isTraceEnabled())
+                                       log.trace("Indexed JAR file " + fileNode);
+                       }
+
+                       JcrUtils.updateLastModified(fileNode);
+
+               } catch (Exception e) {
+                       throw new SlcException("Cannot index jar " + fileNode, e);
+               } finally {
+                       IOUtils.closeQuietly(bi);
+                       IOUtils.closeQuietly(bo);
+                       IOUtils.closeQuietly(jarIn);
+                       JcrUtils.closeQuietly(manifestBinary);
+                       JcrUtils.closeQuietly(fileBinary);
+               }
+
+       }
+
+       private void getI18nValues(Binary fileBinary, Attributes attrs) throws IOException {
+               JarInputStream jarIn = null;
+               try {
+                       jarIn = new JarInputStream(fileBinary.getStream());
+                       String bundleLocalization = null;
+
+                       String blKey = Constants.BUNDLE_LOCALIZATION; // "Bundle-Localization";
+                       Name blkName = new Name(blKey);
+
+                       browse: for (Object obj : attrs.keySet()) {
+                               String value = attrs.getValue((Attributes.Name) obj);
+                               if (value.startsWith("%")) {
+                                       if (attrs.containsKey(blkName)) {
+                                               bundleLocalization = attrs.getValue(blkName);
+                                               break browse;
+                                       }
+                               }
+                       }
+
+                       JarEntry jarEntry = null;
+                       byte[] propBytes = null;
+                       ByteArrayOutputStream baos = null;
+                       browse: if (bundleLocalization != null) {
+                               JarEntry entry = jarIn.getNextJarEntry();
+                               while (entry != null) {
+                                       if (entry.getName().equals(bundleLocalization + ".properties")) {
+                                               jarEntry = entry;
+
+                                               // if(je.getSize() != -1){
+                                               // propBytes = new byte[(int)je.getSize()];
+                                               // int len = (int) je.getSize();
+                                               // int offset = 0;
+                                               // while (offset != len)
+                                               // offset += jarIn.read(propBytes, offset, len -
+                                               // offset);
+                                               // } else {
+                                               baos = new ByteArrayOutputStream();
+                                               while (true) {
+                                                       int qwe = jarIn.read();
+                                                       if (qwe == -1)
+                                                               break;
+                                                       baos.write(qwe);
+                                               }
+                                               propBytes = baos.toByteArray();
+                                               break browse;
+                                       }
+                                       entry = jarIn.getNextJarEntry();
+                               }
+                       }
+
+                       if (jarEntry != null) {
+                               Properties prop = new Properties();
+                               InputStream is = new ByteArrayInputStream(propBytes);
+                               prop.load(is);
+
+                               for (Object obj : attrs.keySet()) {
+                                       String value = attrs.getValue((Attributes.Name) obj);
+                                       if (value.startsWith("%")) {
+                                               String newVal = prop.getProperty(value.substring(1));
+                                               if (newVal != null)
+                                                       attrs.put(obj, newVal);
+                                       }
+                               }
+                       }
+               } catch (RepositoryException e) {
+                       throw new SlcException("Error while reading the jar binary content " + fileBinary, e);
+               } catch (IOException ioe) {
+                       throw new SlcException("unable to get internationalized values", ioe);
+               } finally {
+                       IOUtils.closeQuietly(jarIn);
+               }
+       }
+
+       protected void addOsgiMetadata(Node fileNode, Attributes attrs) throws RepositoryException {
+
+               // TODO remove this ?
+               // Compulsory for the time being, because bundle artifact extends
+               // artifact
+               if (!fileNode.isNodeType(SlcTypes.SLC_ARTIFACT)) {
+                       ArtifactIndexer indexer = new ArtifactIndexer();
+                       indexer.index(fileNode);
+               }
+
+               fileNode.addMixin(SlcTypes.SLC_BUNDLE_ARTIFACT);
+
+               // symbolic name
+               String symbolicName = attrs.getValue(Constants.BUNDLE_SYMBOLICNAME);
+               // make sure there is no directive
+               symbolicName = symbolicName.split(";")[0];
+               fileNode.setProperty(SlcNames.SLC_SYMBOLIC_NAME, symbolicName);
+
+               // direct mapping
+               addAttr(Constants.BUNDLE_SYMBOLICNAME, fileNode, attrs);
+               addAttr(Constants.BUNDLE_NAME, fileNode, attrs);
+               addAttr(Constants.BUNDLE_DESCRIPTION, fileNode, attrs);
+               addAttr(Constants.BUNDLE_MANIFESTVERSION, fileNode, attrs);
+               addAttr(Constants.BUNDLE_CATEGORY, fileNode, attrs);
+               addAttr(Constants.BUNDLE_ACTIVATIONPOLICY, fileNode, attrs);
+               addAttr(Constants.BUNDLE_COPYRIGHT, fileNode, attrs);
+               addAttr(Constants.BUNDLE_VENDOR, fileNode, attrs);
+               addAttr("Bundle-License", fileNode, attrs);
+               addAttr(Constants.BUNDLE_DOCURL, fileNode, attrs);
+               addAttr(Constants.BUNDLE_CONTACTADDRESS, fileNode, attrs);
+               addAttr(Constants.BUNDLE_ACTIVATOR, fileNode, attrs);
+               addAttr(Constants.BUNDLE_UPDATELOCATION, fileNode, attrs);
+               addAttr(Constants.BUNDLE_LOCALIZATION, fileNode, attrs);
+
+               // required execution environment
+               if (attrs.containsKey(new Name(Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT)))
+                       fileNode.setProperty(SlcNames.SLC_ + Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT,
+                                       attrs.getValue(Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT).split(","));
+
+               // bundle classpath
+               if (attrs.containsKey(new Name(Constants.BUNDLE_CLASSPATH)))
+                       fileNode.setProperty(SlcNames.SLC_ + Constants.BUNDLE_CLASSPATH,
+                                       attrs.getValue(Constants.BUNDLE_CLASSPATH).split(","));
+
+               // version
+               Version version = new Version(attrs.getValue(Constants.BUNDLE_VERSION));
+               fileNode.setProperty(SlcNames.SLC_BUNDLE_VERSION, version.toString());
+               cleanSubNodes(fileNode, SlcNames.SLC_ + Constants.BUNDLE_VERSION);
+               Node bundleVersionNode = fileNode.addNode(SlcNames.SLC_ + Constants.BUNDLE_VERSION, SlcTypes.SLC_OSGI_VERSION);
+               mapOsgiVersion(version, bundleVersionNode);
+
+               // fragment
+               cleanSubNodes(fileNode, SlcNames.SLC_ + Constants.FRAGMENT_HOST);
+               if (attrs.containsKey(new Name(Constants.FRAGMENT_HOST))) {
+                       String fragmentHost = attrs.getValue(Constants.FRAGMENT_HOST);
+                       String[] tokens = fragmentHost.split(";");
+                       Node node = fileNode.addNode(SlcNames.SLC_ + Constants.FRAGMENT_HOST, SlcTypes.SLC_FRAGMENT_HOST);
+                       node.setProperty(SlcNames.SLC_SYMBOLIC_NAME, tokens[0]);
+                       for (int i = 1; i < tokens.length; i++) {
+                               if (tokens[i].startsWith(Constants.BUNDLE_VERSION_ATTRIBUTE)) {
+                                       node.setProperty(SlcNames.SLC_BUNDLE_VERSION, attributeValue(tokens[i]));
+                               }
+                       }
+               }
+
+               // imported packages
+               cleanSubNodes(fileNode, SlcNames.SLC_ + Constants.IMPORT_PACKAGE);
+               if (attrs.containsKey(new Name(Constants.IMPORT_PACKAGE))) {
+                       String importPackages = attrs.getValue(Constants.IMPORT_PACKAGE);
+                       List<String> packages = parseCommaSeparated(importPackages);
+                       for (String pkg : packages) {
+                               String[] tokens = pkg.split(";");
+                               Node node = fileNode.addNode(SlcNames.SLC_ + Constants.IMPORT_PACKAGE, SlcTypes.SLC_IMPORTED_PACKAGE);
+                               node.setProperty(SlcNames.SLC_NAME, tokens[0]);
+                               for (int i = 1; i < tokens.length; i++) {
+                                       if (tokens[i].startsWith(Constants.VERSION_ATTRIBUTE)) {
+                                               node.setProperty(SlcNames.SLC_VERSION, attributeValue(tokens[i]));
+                                       } else if (tokens[i].startsWith(Constants.RESOLUTION_DIRECTIVE)) {
+                                               node.setProperty(SlcNames.SLC_OPTIONAL,
+                                                               directiveValue(tokens[i]).equals(Constants.RESOLUTION_OPTIONAL));
+                                       }
+                               }
+                       }
+               }
+
+               // dynamic import package
+               cleanSubNodes(fileNode, SlcNames.SLC_ + Constants.DYNAMICIMPORT_PACKAGE);
+               if (attrs.containsKey(new Name(Constants.DYNAMICIMPORT_PACKAGE))) {
+                       String importPackages = attrs.getValue(Constants.DYNAMICIMPORT_PACKAGE);
+                       List<String> packages = parseCommaSeparated(importPackages);
+                       for (String pkg : packages) {
+                               String[] tokens = pkg.split(";");
+                               Node node = fileNode.addNode(SlcNames.SLC_ + Constants.DYNAMICIMPORT_PACKAGE,
+                                               SlcTypes.SLC_DYNAMIC_IMPORTED_PACKAGE);
+                               node.setProperty(SlcNames.SLC_NAME, tokens[0]);
+                               for (int i = 1; i < tokens.length; i++) {
+                                       if (tokens[i].startsWith(Constants.VERSION_ATTRIBUTE)) {
+                                               node.setProperty(SlcNames.SLC_VERSION, attributeValue(tokens[i]));
+                                       }
+                               }
+                       }
+               }
+
+               // exported packages
+               cleanSubNodes(fileNode, SlcNames.SLC_ + Constants.EXPORT_PACKAGE);
+               if (attrs.containsKey(new Name(Constants.EXPORT_PACKAGE))) {
+                       String exportPackages = attrs.getValue(Constants.EXPORT_PACKAGE);
+                       List<String> packages = parseCommaSeparated(exportPackages);
+                       for (String pkg : packages) {
+                               String[] tokens = pkg.split(";");
+                               Node node = fileNode.addNode(SlcNames.SLC_ + Constants.EXPORT_PACKAGE, SlcTypes.SLC_EXPORTED_PACKAGE);
+                               node.setProperty(SlcNames.SLC_NAME, tokens[0]);
+                               // TODO: are these cleans really necessary?
+                               cleanSubNodes(node, SlcNames.SLC_USES);
+                               cleanSubNodes(node, SlcNames.SLC_VERSION);
+                               for (int i = 1; i < tokens.length; i++) {
+                                       if (tokens[i].startsWith(Constants.VERSION_ATTRIBUTE)) {
+                                               String versionStr = attributeValue(tokens[i]);
+                                               Node versionNode = node.addNode(SlcNames.SLC_VERSION, SlcTypes.SLC_OSGI_VERSION);
+                                               mapOsgiVersion(new Version(versionStr), versionNode);
+                                       } else if (tokens[i].startsWith(Constants.USES_DIRECTIVE)) {
+                                               String usedPackages = directiveValue(tokens[i]);
+                                               // log.debug("uses='" + usedPackages + "'");
+                                               for (String usedPackage : usedPackages.split(",")) {
+                                                       // log.debug("usedPackage='" +
+                                                       // usedPackage +
+                                                       // "'");
+                                                       Node usesNode = node.addNode(SlcNames.SLC_USES, SlcTypes.SLC_JAVA_PACKAGE);
+                                                       usesNode.setProperty(SlcNames.SLC_NAME, usedPackage);
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               // required bundle
+               cleanSubNodes(fileNode, SlcNames.SLC_ + Constants.REQUIRE_BUNDLE);
+               if (attrs.containsKey(new Name(Constants.REQUIRE_BUNDLE))) {
+                       String requireBundle = attrs.getValue(Constants.REQUIRE_BUNDLE);
+                       List<String> bundles = parseCommaSeparated(requireBundle);
+                       for (String bundle : bundles) {
+                               String[] tokens = bundle.split(";");
+                               Node node = fileNode.addNode(SlcNames.SLC_ + Constants.REQUIRE_BUNDLE, SlcTypes.SLC_REQUIRED_BUNDLE);
+                               node.setProperty(SlcNames.SLC_SYMBOLIC_NAME, tokens[0]);
+                               for (int i = 1; i < tokens.length; i++) {
+                                       if (tokens[i].startsWith(Constants.BUNDLE_VERSION_ATTRIBUTE)) {
+                                               node.setProperty(SlcNames.SLC_BUNDLE_VERSION, attributeValue(tokens[i]));
+                                       } else if (tokens[i].startsWith(Constants.RESOLUTION_DIRECTIVE)) {
+                                               node.setProperty(SlcNames.SLC_OPTIONAL,
+                                                               directiveValue(tokens[i]).equals(Constants.RESOLUTION_OPTIONAL));
+                                       }
+                               }
+                       }
+               }
+
+       }
+
+       private void addAttr(String key, Node node, Attributes attrs) throws RepositoryException {
+               addAttr(new Name(key), node, attrs);
+       }
+
+       private void addAttr(Name key, Node node, Attributes attrs) throws RepositoryException {
+               if (attrs.containsKey(key)) {
+                       String value = attrs.getValue(key);
+                       node.setProperty(SlcNames.SLC_ + key, value);
+               }
+       }
+
+       private void cleanSubNodes(Node node, String name) throws RepositoryException {
+               if (node.hasNode(name)) {
+                       NodeIterator nit = node.getNodes(name);
+                       while (nit.hasNext())
+                               nit.nextNode().remove();
+               }
+       }
+
+       private String attributeValue(String str) {
+               return extractValue(str, "=");
+       }
+
+       private String directiveValue(String str) {
+               return extractValue(str, ":=");
+       }
+
+       private String extractValue(String str, String eq) {
+               String[] tokens = str.split(eq);
+               // String key = tokens[0];
+               String value = tokens[1].trim();
+               // TODO: optimize?
+               if (value.startsWith("\""))
+                       value = value.substring(1);
+               if (value.endsWith("\""))
+                       value = value.substring(0, value.length() - 1);
+               return value;
+       }
+
+       /** Parse package list with nested directive with ',' */
+       private List<String> parseCommaSeparated(String str) {
+               List<String> res = new ArrayList<String>();
+               StringBuffer curr = new StringBuffer("");
+               boolean in = false;
+               for (char c : str.toCharArray()) {
+                       if (c == ',') {
+                               if (!in) {// new package
+                                       res.add(curr.toString());
+                                       curr = new StringBuffer("");
+                               } else {// a ',' within " "
+                                       curr.append(c);
+                               }
+                       } else if (c == '\"') {
+                               in = !in;
+                               curr.append(c);
+                       } else {
+                               curr.append(c);
+                       }
+               }
+               res.add(curr.toString());
+               // log.debug(res);
+               return res;
+       }
+
+       protected void mapOsgiVersion(Version version, Node versionNode) throws RepositoryException {
+               versionNode.setProperty(SlcNames.SLC_AS_STRING, version.toString());
+               versionNode.setProperty(SlcNames.SLC_MAJOR, version.getMajor());
+               versionNode.setProperty(SlcNames.SLC_MINOR, version.getMinor());
+               versionNode.setProperty(SlcNames.SLC_MICRO, version.getMicro());
+               if (!version.getQualifier().equals(""))
+                       versionNode.setProperty(SlcNames.SLC_QUALIFIER, version.getQualifier());
+       }
+
+       public void setForce(Boolean force) {
+               this.force = force;
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/JavaRepoManager.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/JavaRepoManager.java
new file mode 100644 (file)
index 0000000..4d7c3ef
--- /dev/null
@@ -0,0 +1,7 @@
+package org.argeo.slc.repo;
+
+/** Java-specific operations */
+public interface JavaRepoManager {
+       public void createWorkspace(String workspaceName);
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/MavenProxyService.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/MavenProxyService.java
new file mode 100644 (file)
index 0000000..81261fe
--- /dev/null
@@ -0,0 +1,7 @@
+package org.argeo.slc.repo;
+
+import org.argeo.jcr.proxy.ResourceProxy;
+
+/** Marker interface (useful for OSGi servcies references), maybe extended later */
+public interface MavenProxyService extends ResourceProxy {
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/ModularDistributionFactory.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/ModularDistributionFactory.java
new file mode 100644 (file)
index 0000000..19627d8
--- /dev/null
@@ -0,0 +1,519 @@
+package org.argeo.slc.repo;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.CategoryNameVersion;
+import org.argeo.slc.NameVersion;
+import org.argeo.slc.SlcException;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.osgi.framework.Constants;
+
+/**
+ * Creates a jar bundle from an ArgeoOsgiDistribution. This jar is then
+ * persisted and indexed in a java repository using the OSGI Factory.
+ * 
+ * It does the following <list>
+ * <li>Creates a Manifest</li>
+ * <li>Creates files indexes (csv, feature.xml ...)</li>
+ * <li>Populate the corresponding jar</li>
+ * <li>Save it in the repository</li>
+ * <li>Index the node and creates corresponding sha1 and md5 files</li> </list>
+ * 
+ */
+public class ModularDistributionFactory implements Runnable {
+
+       private OsgiFactory osgiFactory;
+       private Session javaSession;
+       private ArgeoOsgiDistribution osgiDistribution;
+       private String modularDistributionSeparator = ",";
+       private String artifactBasePath = RepoConstants.DEFAULT_ARTIFACTS_BASE_PATH;
+       private String artifactType = "jar";
+
+       // Constants
+       private final static String CSV_FILE_NAME = "modularDistribution.csv";
+       private final DateFormat snapshotTimestamp = new SimpleDateFormat("YYYYMMddhhmm");
+
+       // private final static String FEATURE_FILE_NAME = "feature.xml";
+       // private static int BUFFER_SIZE = 10240;
+
+       /** Convenience constructor with minimal configuration */
+       public ModularDistributionFactory(OsgiFactory osgiFactory, ArgeoOsgiDistribution osgiDistribution) {
+               this.osgiFactory = osgiFactory;
+               this.osgiDistribution = osgiDistribution;
+       }
+
+       @Override
+       public void run() {
+               byte[] distFile = null;
+               try {
+                       javaSession = osgiFactory.openJavaSession();
+
+                       if (artifactType == "jar")
+                               distFile = generateJarFile();
+                       else if (artifactType == "pom")
+                               distFile = generatePomFile();
+                       else
+                               throw new SlcException("Unimplemented distribution artifact type: " + artifactType + " for "
+                                               + osgiDistribution.toString());
+
+                       // Save in java repository
+                       Artifact osgiArtifact = new DefaultArtifact(osgiDistribution.getCategory(), osgiDistribution.getName(),
+                                       artifactType, osgiDistribution.getVersion());
+
+                       Node distNode = RepoUtils.copyBytesAsArtifact(javaSession.getNode(artifactBasePath), osgiArtifact,
+                                       distFile);
+
+                       // index
+                       osgiFactory.indexNode(distNode);
+
+                       // We use a specific session. Save before closing
+                       javaSession.save();
+               } catch (RepositoryException e) {
+                       throw new SlcException(
+                                       "JCR error while persisting modular distribution in JCR " + osgiDistribution.toString(), e);
+               } finally {
+                       JcrUtils.logoutQuietly(javaSession);
+               }
+       }
+
+       private byte[] generateJarFile() {
+               ByteArrayOutputStream byteOut = null;
+               JarOutputStream jarOut = null;
+               try {
+                       byteOut = new ByteArrayOutputStream();
+                       jarOut = new JarOutputStream(byteOut, createManifest());
+                       // Create various indexes
+                       addToJar(createCsvDescriptor(), CSV_FILE_NAME, jarOut);
+                       jarOut.close();
+                       return byteOut.toByteArray();
+               } catch (IOException e) {
+                       throw new SlcException("IO error while generating modular distribution " + osgiDistribution.toString(), e);
+               } finally {
+                       IOUtils.closeQuietly(byteOut);
+                       IOUtils.closeQuietly(jarOut);
+               }
+       }
+
+       // private void indexDistribution(Node distNode) throws RepositoryException
+       // {
+       // distNode.addMixin(SlcTypes.SLC_MODULAR_DISTRIBUTION);
+       // distNode.addMixin(SlcTypes.SLC_CATEGORIZED_NAME_VERSION);
+       // distNode.setProperty(SlcNames.SLC_CATEGORY,
+       // osgiDistribution.getCategory());
+       // distNode.setProperty(SlcNames.SLC_NAME, osgiDistribution.getName());
+       // distNode.setProperty(SlcNames.SLC_VERSION,
+       // osgiDistribution.getVersion());
+       //
+       // if (distNode.hasNode(SlcNames.SLC_MODULES))
+       // distNode.getNode(SlcNames.SLC_MODULES).remove();
+       // Node modules = distNode.addNode(SlcNames.SLC_MODULES,
+       // NodeType.NT_UNSTRUCTURED);
+       //
+       // for (Iterator<? extends NameVersion> it = osgiDistribution
+       // .nameVersions(); it.hasNext();)
+       // addModule(modules, it.next());
+       // }
+
+       private Manifest createManifest() {
+               Manifest manifest = new Manifest();
+
+               // TODO make this configurable
+               manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
+//             addManifestAttribute(manifest, Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT, "JavaSE-1.8");
+               addManifestAttribute(manifest, Constants.BUNDLE_VENDOR, "Argeo");
+               addManifestAttribute(manifest, Constants.BUNDLE_MANIFESTVERSION, "2");
+//             addManifestAttribute(manifest, "Bundle-License", "http://www.apache.org/licenses/LICENSE-2.0.txt");
+
+               // TODO define a user friendly name
+               addManifestAttribute(manifest, Constants.BUNDLE_NAME, osgiDistribution.getName());
+
+               // Categorized name version
+               addManifestAttribute(manifest, RepoConstants.SLC_CATEGORY_ID, osgiDistribution.getCategory());
+               addManifestAttribute(manifest, Constants.BUNDLE_SYMBOLICNAME, osgiDistribution.getName());
+               String version = osgiDistribution.getVersion();
+               if (version.endsWith("-SNAPSHOT")) {
+                       version = version.substring(0, version.length() - "-SNAPSHOT".length());
+                       version = version + ".SNAPSHOT-r" + snapshotTimestamp.format(new Date());
+               }
+               addManifestAttribute(manifest, Constants.BUNDLE_VERSION, version);
+
+               return manifest;
+       }
+
+       private void addManifestAttribute(Manifest manifest, String name, String value) {
+               manifest.getMainAttributes().put(new Attributes.Name(name), value);
+       }
+
+       private byte[] createCsvDescriptor() {
+               Writer writer = null;
+               try {
+                       // FIXME remove use of tmp file.
+                       File tmpFile = File.createTempFile("modularDistribution", "csv");
+                       tmpFile.deleteOnExit();
+                       writer = new FileWriter(tmpFile);
+                       // Populate the file
+                       for (Iterator<? extends NameVersion> it = osgiDistribution.nameVersions(); it.hasNext();)
+                               writer.write(getCsvLine(it.next()));
+                       writer.flush();
+                       return FileUtils.readFileToByteArray(tmpFile);
+               } catch (Exception e) {
+                       throw new SlcException("unable to create csv distribution file for " + osgiDistribution.toString(), e);
+               } finally {
+                       IOUtils.closeQuietly(writer);
+               }
+       }
+
+       @SuppressWarnings("unused")
+       private byte[] createFeatureDescriptor() {
+               // Directly retrieved from Argeo maven plugin
+               // Does not work due to the lack of org.codehaus.plexus/plexus-archiver
+               // third party dependency
+
+               throw new SlcException("Unimplemented method");
+
+               // // protected void writeFeatureDescriptor() throws
+               // MojoExecutionException {
+               // File featureDesc = File.createTempFile("feature", "xml");
+               // featureDesc.deleteOnExit();
+               //
+               // Writer writer = null;
+               // try {
+               // writer = new FileWriter(featureDesc);
+               // PrettyPrintXMLWriter xmlWriter = new PrettyPrintXMLWriter(writer);
+               // xmlWriter.startElement("feature");
+               // xmlWriter.addAttribute("id", project.getArtifactId());
+               // xmlWriter.addAttribute("label", project.getName());
+               //
+               // // Version
+               // String projectVersion = project.getVersion();
+               // int indexSnapshot = projectVersion.indexOf("-SNAPSHOT");
+               // if (indexSnapshot > -1)
+               // projectVersion = projectVersion.substring(0, indexSnapshot);
+               // projectVersion = projectVersion + ".qualifier";
+               //
+               // // project.
+               // xmlWriter.addAttribute("version", projectVersion);
+               //
+               // Organization organization = project.getOrganization();
+               // if (organization != null && organization.getName() != null)
+               // xmlWriter.addAttribute("provider-name", organization.getName());
+               //
+               // if (project.getDescription() != null || project.getUrl() != null) {
+               // xmlWriter.startElement("description");
+               // if (project.getUrl() != null)
+               // xmlWriter.addAttribute("url", project.getUrl());
+               // if (project.getDescription() != null)
+               // xmlWriter.writeText(project.getDescription());
+               // xmlWriter.endElement();// description
+               // }
+               //
+               // if (feature != null && feature.getCopyright() != null
+               // || (organization != null && organization.getUrl() != null)) {
+               // xmlWriter.startElement("copyright");
+               // if (organization != null && organization.getUrl() != null)
+               // xmlWriter.addAttribute("url", organization.getUrl());
+               // if (feature.getCopyright() != null)
+               // xmlWriter.writeText(feature.getCopyright());
+               // xmlWriter.endElement();// copyright
+               // }
+               //
+               // if (feature != null && feature.getUpdateSite() != null) {
+               // xmlWriter.startElement("url");
+               // xmlWriter.startElement("update");
+               // xmlWriter.addAttribute("url", feature.getUpdateSite());
+               // xmlWriter.endElement();// update
+               // xmlWriter.endElement();// url
+               // }
+               //
+               // List licenses = project.getLicenses();
+               // if (licenses.size() > 0) {
+               // // take the first one
+               // License license = (License) licenses.get(0);
+               // xmlWriter.startElement("license");
+               //
+               // if (license.getUrl() != null)
+               // xmlWriter.addAttribute("url", license.getUrl());
+               // if (license.getComments() != null)
+               // xmlWriter.writeText(license.getComments());
+               // else if (license.getName() != null)
+               // xmlWriter.writeText(license.getName());
+               // xmlWriter.endElement();// license
+               // }
+               //
+               // // deploymentRepository.pathOf(null);
+               // if (jarDirectory == null) {
+               // Set dependencies = mavenDependencyManager
+               // .getTransitiveProjectDependencies(project, remoteRepos,
+               // local);
+               // // // protected void writeFeatureDescriptor() throws
+               // MojoExecutionException {
+               // File featureDesc = File.createTempFile("feature", "xml");
+               // featureDesc.deleteOnExit();
+               //
+               // Writer writer = null;
+               // try {
+               // writer = new FileWriter(featureDesc);
+               // PrettyPrintXMLWriter xmlWriter = new PrettyPrintXMLWriter(writer);
+               // xmlWriter.startElement("feature");
+               // xmlWriter.addAttribute("id", project.getArtifactId());
+               // xmlWriter.addAttribute("label", project.getName());
+               //
+               // // Version
+               // String projectVersion = project.getVersion();
+               // int indexSnapshot = projectVersion.indexOf("-SNAPSHOT");
+               // if (indexSnapshot > -1)
+               // projectVersion = projectVersion.substring(0, indexSnapshot);
+               // projectVersion = projectVersion + ".qualifier";
+               //
+               // // project.
+               // xmlWriter.addAttribute("version", projectVersion);
+               //
+               // Organization organization = project.getOrganization();
+               // if (organization != null && organization.getName() != null)
+               // xmlWriter.addAttribute("provider-name", organization.getName());
+               //
+               // if (project.getDescription() != null || project.getUrl() != null) {
+               // xmlWriter.startElement("description");
+               // if (project.getUrl() != null)
+               // xmlWriter.addAttribute("url", project.getUrl());
+               // if (project.getDescription() != null)
+               // xmlWriter.writeText(project.getDescription());
+               // xmlWriter.endElement();// description
+               // }
+               //
+               // if (feature != null && feature.getCopyright() != null
+               // || (organization != null && organization.getUrl() != null)) {
+               // xmlWriter.startElement("copyright");
+               // if (organization != null && organization.getUrl() != null)
+               // xmlWriter.addAttribute("url", organization.getUrl());
+               // if (feature.getCopyright() != null)
+               // xmlWriter.writeText(feature.getCopyright());
+               // xmlWriter.endElement();// copyright
+               // }
+               //
+               // if (feature != null && feature.getUpdateSite() != null) {
+               // xmlWriter.startElement("url");
+               // xmlWriter.startElement("update");
+               // xmlWriter.addAttribute("url", feature.getUpdateSite());
+               // xmlWriter.endElement();// update
+               // xmlWriter.endElement();// url
+               // }
+               //
+               // List licenses = project.getLicenses();
+               // if (licenses.size() > 0) {
+               // // take the first one
+               // License license = (License) licenses.get(0);
+               // xmlWriter.startElement("license");
+               //
+               // if (license.getUrl() != null)
+               // xmlWriter.addAttribute("url", license.getUrl());
+               // if (license.getComments() != null)
+               // xmlWriter.writeText(license.getComments());
+               // else if (license.getName() != null)
+               // xmlWriter.writeText(license.getName());
+               // xmlWriter.endElement();// license
+               // }
+               //
+               // // deploymentRepository.pathOf(null);
+               // if (jarDirectory == null) {
+               // Set dependencies = mavenDependencyManager
+               // .getTransitiveProjectDependencies(project, remoteRepos,
+               // local);
+               // for (Iterator it = dependencies.iterator(); it.hasNext();) {
+               // Artifact artifact = (Artifact) it.next();
+               // writeFeaturePlugin(xmlWriter, artifact.getFile());
+               // }
+               // } else {
+               // // TODO: filter jars
+               // File[] jars = jarDirectory.listFiles();
+               // if (jars == null)
+               // throw new MojoExecutionException("No jar found in "
+               // + jarDirectory);
+               // for (int i = 0; i < jars.length; i++) {
+               // writeFeaturePlugin(xmlWriter, jars[i]);
+               // }
+               // }
+               //
+               // xmlWriter.endElement();// feature
+               //
+               // if (getLog().isDebugEnabled())
+               // getLog().debug("Wrote Eclipse feature descriptor.");
+               // } catch (Exception e) {
+               // throw new MojoExecutionException("Cannot write feature descriptor",
+               // e);
+               // } finally {
+               // IOUtil.close(writer);
+               // }for (Iterator it = dependencies.iterator(); it.hasNext();) {
+               // Artifact artifact = (Artifact) it.next();
+               // writeFeaturePlugin(xmlWriter, artifact.getFile());
+               // }
+               // } else {
+               // // TODO: filter jars
+               // File[] jars = jarDirectory.listFiles();
+               // if (jars == null)
+               // throw new MojoExecutionException("No jar found in "
+               // + jarDirectory);
+               // for (int i = 0; i < jars.length; i++) {
+               // writeFeaturePlugin(xmlWriter, jars[i]);
+               // }
+               // }
+               //
+               // xmlWriter.endElement();// feature
+               //
+               // if (getLog().isDebugEnabled())
+               // getLog().debug("Wrote Eclipse feature descriptor.");
+               // } catch (Exception e) {
+               // throw new MojoExecutionException("Cannot write feature descriptor",
+               // e);
+               // } finally {
+               // IOUtil.close(writer);
+               // }
+       }
+
+       /** Create an Aether like distribution artifact */
+       private byte[] generatePomFile() {
+               StringBuilder b = new StringBuilder();
+               // XML header
+               b.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
+               b.append(
+                               "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n");
+               b.append("<modelVersion>4.0.0</modelVersion>");
+
+               // Artifact
+               b.append("<groupId>").append(osgiDistribution.getCategory()).append("</groupId>\n");
+               b.append("<artifactId>").append(osgiDistribution.getName()).append("</artifactId>\n");
+               b.append("<version>").append(osgiDistribution.getVersion()).append("</version>\n");
+               b.append("<packaging>pom</packaging>\n");
+               // p.append("<name>").append("Bundle Name").append("</name>\n");
+               // p.append("<description>").append("Bundle
+               // Description").append("</description>\n");
+
+               // Dependencies
+               b.append("<dependencies>\n");
+               for (Iterator<? extends NameVersion> it = osgiDistribution.nameVersions(); it.hasNext();) {
+                       NameVersion nameVersion = it.next();
+                       if (!(nameVersion instanceof CategoryNameVersion))
+                               throw new SlcException("Unsupported type " + nameVersion.getClass());
+                       CategoryNameVersion nv = (CategoryNameVersion) nameVersion;
+                       b.append(getDependencySnippet(nv, false));
+               }
+               b.append("</dependencies>\n");
+
+               // Dependency management
+               b.append("<dependencyManagement>\n");
+               b.append("<dependencies>\n");
+
+               for (Iterator<? extends NameVersion> it = osgiDistribution.nameVersions(); it.hasNext();)
+                       b.append(getDependencySnippet((CategoryNameVersion) it.next(), true));
+               b.append("</dependencies>\n");
+               b.append("</dependencyManagement>\n");
+
+               b.append("</project>\n");
+               return b.toString().getBytes();
+       }
+
+       private String getDependencySnippet(CategoryNameVersion cnv, boolean includeVersion) { // , String type, String
+                                                                                                                                                                                               // scope
+               StringBuilder b = new StringBuilder();
+               b.append("<dependency>\n");
+               b.append("\t<groupId>").append(cnv.getCategory()).append("</groupId>\n");
+               b.append("\t<artifactId>").append(cnv.getName()).append("</artifactId>\n");
+               if (includeVersion)
+                       b.append("\t<version>").append(cnv.getVersion()).append("</version>\n");
+               // if (type!= null)
+               // p.append("\t<type>").append(type).append("</type>\n");
+               // if (type!= null)
+               // p.append("\t<scope>").append(scope).append("</scope>\n");
+               b.append("</dependency>\n");
+               return b.toString();
+       }
+
+       // Helpers
+       private void addToJar(byte[] content, String name, JarOutputStream target) throws IOException {
+               ByteArrayInputStream in = null;
+               try {
+                       target.putNextEntry(new JarEntry(name));
+                       in = new ByteArrayInputStream(content);
+                       byte[] buffer = new byte[1024];
+                       while (true) {
+                               int count = in.read(buffer);
+                               if (count == -1)
+                                       break;
+                               target.write(buffer, 0, count);
+                       }
+                       target.closeEntry();
+               } finally {
+                       IOUtils.closeQuietly(in);
+               }
+       }
+
+       private String getCsvLine(NameVersion nameVersion) throws RepositoryException {
+               if (!(nameVersion instanceof CategoryNameVersion))
+                       throw new SlcException("Unsupported type " + nameVersion.getClass());
+               CategoryNameVersion cnv = (CategoryNameVersion) nameVersion;
+               StringBuilder builder = new StringBuilder();
+
+               builder.append(cnv.getName());
+               builder.append(modularDistributionSeparator);
+               builder.append(nameVersion.getVersion());
+               builder.append(modularDistributionSeparator);
+               builder.append(cnv.getCategory().replace('.', '/'));
+               // MavenConventionsUtils.groupPath("", cnv.getCategory());
+               builder.append('/');
+               builder.append(cnv.getName());
+               builder.append('/');
+               builder.append(cnv.getVersion());
+               builder.append('/');
+               builder.append(cnv.getName());
+               builder.append('-');
+               builder.append(cnv.getVersion());
+               builder.append('.');
+               // TODO make this dynamic
+               builder.append("jar");
+               builder.append("\n");
+
+               return builder.toString();
+       }
+
+       /** Enable dependency injection */
+       public void setOsgiFactory(OsgiFactory osgiFactory) {
+               this.osgiFactory = osgiFactory;
+       }
+
+       public void setOsgiDistribution(ArgeoOsgiDistribution osgiDistribution) {
+               this.osgiDistribution = osgiDistribution;
+       }
+
+       public void setModularDistributionSeparator(String modularDistributionSeparator) {
+               this.modularDistributionSeparator = modularDistributionSeparator;
+       }
+
+       public void setArtifactBasePath(String artifactBasePath) {
+               this.artifactBasePath = artifactBasePath;
+       }
+
+       public void setArtifactType(String artifactType) {
+               this.artifactType = artifactType;
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/ModularDistributionIndexer.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/ModularDistributionIndexer.java
new file mode 100644 (file)
index 0000000..7d854a1
--- /dev/null
@@ -0,0 +1,213 @@
+package org.argeo.slc.repo;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.StringTokenizer;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+
+import javax.jcr.Binary;
+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.apache.commons.io.IOUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.CategoryNameVersion;
+import org.argeo.slc.DefaultCategoryNameVersion;
+import org.argeo.slc.NameVersion;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.argeo.slc.build.Distribution;
+import org.argeo.slc.repo.maven.AetherUtils;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.osgi.framework.Constants;
+
+/**
+ * Create or update JCR meta-data for an SLC Modular Distribution
+ * 
+ * Currently, following types are managed: <list>
+ * <li>* .jar: dependency artifacts with csv index</li>
+ * <li>@Deprecated : .pom: artifact (binaries) that indexes a group, the .pom
+ * file contains a tag "dependencyManagement" that list all modules</li> </list>
+ */
+public class ModularDistributionIndexer implements NodeIndexer, SlcNames {
+       private final static CmsLog log = CmsLog.getLog(ModularDistributionIndexer.class);
+
+       // Constants for csv indexing
+       private final static String INDEX_FILE_NAME = "modularDistribution.csv";
+       private String separator = ",";
+
+       private Manifest manifest;
+
+       public Boolean support(String path) {
+               if (FilenameUtils.getExtension(path).equals("jar"))
+                       return true;
+               return false;
+       }
+
+       public void index(Node fileNode) {
+               Binary fileBinary = null;
+               try {
+                       String fileNodePath = fileNode.getPath();
+                       if (!support(fileNodePath))
+                               return;
+
+                       if (!fileNode.isNodeType(NodeType.NT_FILE))
+                               return;
+
+                       Node contentNode = fileNode.getNode(Node.JCR_CONTENT);
+                       fileBinary = contentNode.getProperty(Property.JCR_DATA).getBinary();
+
+                       MyModularDistribution currDist = null;
+                       if (FilenameUtils.getExtension(fileNode.getPath()).equals("jar"))
+                               currDist = listModulesFromCsvIndex(fileNode, fileBinary);
+
+                       if (fileNode.isNodeType(SlcTypes.SLC_MODULAR_DISTRIBUTION) || currDist == null
+                                       || !currDist.nameVersions().hasNext())
+                               return; // already indexed or no modules found
+                       else {
+                               fileNode.addMixin(SlcTypes.SLC_MODULAR_DISTRIBUTION);
+                               fileNode.addMixin(SlcTypes.SLC_CATEGORIZED_NAME_VERSION);
+                               if (currDist.getCategory() != null)
+                                       fileNode.setProperty(SLC_CATEGORY, currDist.getCategory());
+                               fileNode.setProperty(SLC_NAME, currDist.getName());
+                               fileNode.setProperty(SLC_VERSION, currDist.getVersion());
+                               indexDistribution(currDist, fileNode);
+                       }
+
+                       if (log.isTraceEnabled())
+                               log.trace("Indexed " + fileNode + " as modular distribution");
+               } catch (Exception e) {
+                       throw new SlcException("Cannot list dependencies from " + fileNode, e);
+               } finally {
+                       JcrUtils.closeQuietly(fileBinary);
+               }
+       }
+
+       private void indexDistribution(ArgeoOsgiDistribution osgiDist, Node distNode) throws RepositoryException {
+               distNode.addMixin(SlcTypes.SLC_MODULAR_DISTRIBUTION);
+               distNode.addMixin(SlcTypes.SLC_CATEGORIZED_NAME_VERSION);
+               distNode.setProperty(SlcNames.SLC_CATEGORY, osgiDist.getCategory());
+               distNode.setProperty(SlcNames.SLC_NAME, osgiDist.getName());
+               distNode.setProperty(SlcNames.SLC_VERSION, osgiDist.getVersion());
+               if (distNode.hasNode(SLC_MODULES))
+                       distNode.getNode(SLC_MODULES).remove();
+               Node modules = distNode.addNode(SLC_MODULES, NodeType.NT_UNSTRUCTURED);
+
+               for (Iterator<? extends NameVersion> it = osgiDist.nameVersions(); it.hasNext();)
+                       addModule(modules, it.next());
+       }
+
+       // Helpers
+       private Node addModule(Node modules, NameVersion nameVersion) throws RepositoryException {
+               CategoryNameVersion cnv = (CategoryNameVersion) nameVersion;
+               Node moduleCoord = null;
+               moduleCoord = modules.addNode(cnv.getName(), SlcTypes.SLC_MODULE_COORDINATES);
+               moduleCoord.setProperty(SlcNames.SLC_CATEGORY, cnv.getCategory());
+               moduleCoord.setProperty(SlcNames.SLC_NAME, cnv.getName());
+               moduleCoord.setProperty(SlcNames.SLC_VERSION, cnv.getVersion());
+               return moduleCoord;
+       }
+
+       private MyModularDistribution listModulesFromCsvIndex(Node fileNode, Binary fileBinary) {
+               JarInputStream jarIn = null;
+               BufferedReader reader = null;
+               try {
+                       jarIn = new JarInputStream(fileBinary.getStream());
+
+                       List<CategoryNameVersion> modules = new ArrayList<CategoryNameVersion>();
+
+                       // meta data
+                       manifest = jarIn.getManifest();
+                       if (manifest == null) {
+                               log.error(fileNode + " has no MANIFEST");
+                               return null;
+                       }
+                       String category = manifest.getMainAttributes().getValue(RepoConstants.SLC_CATEGORY_ID);
+                       String name = manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
+                       String version = manifest.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
+
+                       Artifact distribution = new DefaultArtifact(category, name, "jar", version);
+                       // Retrieve the index file
+                       JarEntry indexEntry;
+                       while ((indexEntry = jarIn.getNextJarEntry()) != null) {
+                               String entryName = indexEntry.getName();
+                               if (entryName.equals(INDEX_FILE_NAME)) {
+                                       break;
+                               }
+                               try {
+                                       jarIn.closeEntry();
+                               } catch (SecurityException se) {
+                                       log.error("Invalid signature file digest " + "for Manifest main attributes: " + entryName
+                                                       + " while looking for an index in bundle " + name);
+                               }
+                       }
+                       if (indexEntry == null)
+                               return null; // Not a modular definition
+
+                       if (category == null) {
+                               log.warn("Modular definition found but no " + RepoConstants.SLC_CATEGORY_ID + " in " + fileNode);
+                       }
+
+                       // Process the index
+                       reader = new BufferedReader(new InputStreamReader(jarIn));
+                       String line = null;
+                       while ((line = reader.readLine()) != null) {
+                               StringTokenizer st = new StringTokenizer(line, separator);
+                               st.nextToken(); // moduleName
+                               st.nextToken(); // moduleVersion
+                               String relativeUrl = st.nextToken();
+                               Artifact currModule = AetherUtils.convertPathToArtifact(relativeUrl, null);
+                               modules.add(new DefaultCategoryNameVersion(currModule.getGroupId(), currModule.getArtifactId(),
+                                               currModule.getVersion()));
+                       }
+                       return new MyModularDistribution(distribution, modules);
+               } catch (Exception e) {
+                       throw new SlcException("Cannot list artifacts", e);
+               } finally {
+                       IOUtils.closeQuietly(jarIn);
+                       IOUtils.closeQuietly(reader);
+               }
+       }
+
+       /**
+        * A consistent and versioned OSGi distribution, which can be built and tested.
+        */
+       private class MyModularDistribution extends ArtifactDistribution implements ArgeoOsgiDistribution {
+
+               private List<CategoryNameVersion> modules;
+
+               public MyModularDistribution(Artifact artifact, List<CategoryNameVersion> modules) {
+                       super(artifact);
+                       this.modules = modules;
+               }
+
+               public Iterator<CategoryNameVersion> nameVersions() {
+                       return modules.iterator();
+               }
+
+               // Modular distribution interface methods. Not yet used.
+               public Distribution getModuleDistribution(String moduleName, String moduleVersion) {
+                       return null;
+               }
+
+               public Object getModulesDescriptor(String descriptorType) {
+                       return null;
+               }
+       }
+
+       /** Separator used to parse the tabular file, default is "," */
+       public void setSeparator(String modulesUrlSeparator) {
+               this.separator = modulesUrlSeparator;
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/NodeIndexer.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/NodeIndexer.java
new file mode 100644 (file)
index 0000000..374ad85
--- /dev/null
@@ -0,0 +1,29 @@
+package org.argeo.slc.repo;
+
+import javax.jcr.Node;
+import javax.jcr.observation.EventListener;
+
+/**
+ * Adds metadata to an existing node, ideally via observation after it has been
+ * added. There is a similar concept in ModeShape with which this abstraction
+ * may be merged in the future.
+ */
+public interface NodeIndexer {
+       /**
+        * Whether the node at this path will be supported. This is typically use in
+        * an {@link EventListener} before the node is loaded, and would apply on
+        * information contained in the path / file name: file extension, base path,
+        * etc. If the node needs to be loaded, the recommended approach is to
+        * return <code>true</code> here and wait for index to be called, possibly
+        * returning without processing if the node should not be indexed. While
+        * not strictly a requirement, this avoids to open sessions in the indexer,
+        * centralizing such tasks in the caller.
+        */
+       public Boolean support(String path);
+
+       /**
+        * Adds the metadata. This is the responsibility of the caller to save the
+        * underlying session.
+        */
+       public void index(Node node);
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/NodeIndexerVisitor.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/NodeIndexerVisitor.java
new file mode 100644 (file)
index 0000000..c0f90d1
--- /dev/null
@@ -0,0 +1,48 @@
+package org.argeo.slc.repo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.ItemVisitor;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+
+/**
+ * Recursively visit a sub tree and apply the list of node indexer on supported
+ * nodes.
+ */
+public class NodeIndexerVisitor implements ItemVisitor {
+       /** order may be important */
+       private List<NodeIndexer> nodeIndexers = new ArrayList<NodeIndexer>();
+
+       public NodeIndexerVisitor() {
+       }
+
+       /** Convenience constructor */
+       public NodeIndexerVisitor(NodeIndexer nodeIndexer) {
+               nodeIndexers.add(nodeIndexer);
+       }
+
+       public NodeIndexerVisitor(List<NodeIndexer> nodeIndexers) {
+               this.nodeIndexers = nodeIndexers;
+       }
+
+       public void visit(Node node) throws RepositoryException {
+               for (NodeIndexer nodeIndexer : nodeIndexers)
+                       if (nodeIndexer.support(node.getPath()))
+                               nodeIndexer.index(node);
+
+               for (NodeIterator it = node.getNodes(); it.hasNext();)
+                       visit(it.nextNode());
+       }
+
+       public void visit(Property property) throws RepositoryException {
+       }
+
+       public void setNodeIndexers(List<NodeIndexer> nodeIndexers) {
+               this.nodeIndexers = nodeIndexers;
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/OsgiBundlesProvider.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/OsgiBundlesProvider.java
new file mode 100644 (file)
index 0000000..de5f03c
--- /dev/null
@@ -0,0 +1,12 @@
+package org.argeo.slc.repo;
+
+import java.util.List;
+
+/**
+ * Provides OSGi bundles either by linking to them, by wrapping existing
+ * archives or by building them.
+ */
+public interface OsgiBundlesProvider {
+       /** The provided bundles in the order they will be retrieved/wrapped/built. */
+       public List<ArtifactDistribution> provides();
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/OsgiFactory.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/OsgiFactory.java
new file mode 100644 (file)
index 0000000..8178493
--- /dev/null
@@ -0,0 +1,28 @@
+package org.argeo.slc.repo;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+/** OSGi Factory */
+public interface OsgiFactory {
+       public Session openJavaSession() throws RepositoryException;
+
+       public Session openDistSession() throws RepositoryException;
+
+       public void indexNode(Node node);
+
+       /**
+        * Provide access to a third party archive in the 'dist' repository,
+        * downloading it if it is not available.
+        */
+       public Node getDist(Session distSession, String uri)
+                       throws RepositoryException;
+
+       /**
+        * Provide access to a cached maven ardifact identified by its coordinates
+        * the 'dist' repository, downloading it if it is not available.
+        */
+       public Node getMaven(Session distSession, String coords)
+                       throws RepositoryException;
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/PdeSourcesIndexer.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/PdeSourcesIndexer.java
new file mode 100644 (file)
index 0000000..6b8aee9
--- /dev/null
@@ -0,0 +1,108 @@
+package org.argeo.slc.repo;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.commons.io.FilenameUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.NameVersion;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.repo.maven.AetherUtils;
+import org.argeo.slc.repo.maven.MavenConventionsUtils;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+
+/**
+ * Creates pde sources from a source {@link Artifact} with name
+ * "...-sources.jar"
+ */
+public class PdeSourcesIndexer implements NodeIndexer {
+       private CmsLog log = CmsLog.getLog(PdeSourcesIndexer.class);
+
+       private String artifactBasePath = RepoConstants.DEFAULT_ARTIFACTS_BASE_PATH;
+
+       // private ArtifactIndexer artifactIndexer;
+       // private JarFileIndexer jarFileIndexer;
+
+       // public PdeSourcesIndexer(){
+       // // ArtifactIndexer artifactIndexer,
+       // // JarFileIndexer jarFileIndexer) {
+       // // this.artifactIndexer = artifactIndexer;
+       // // this.jarFileIndexer = jarFileIndexer;
+       // }
+
+       public Boolean support(String path) {
+               // TODO implement clean management of same name siblings
+               String name = FilenameUtils.getBaseName(path);
+               // int lastInd = name.lastIndexOf("[");
+               // if (lastInd != -1)
+               // name = name.substring(0, lastInd);
+               return name.endsWith("-sources") && FilenameUtils.getExtension(path).equals("jar");
+       }
+
+       public void index(Node sourcesNode) {
+               try {
+                       if (!support(sourcesNode.getPath()))
+                               return;
+
+                       packageSourcesAsPdeSource(sourcesNode);
+               } catch (Exception e) {
+                       throw new SlcException("Cannot generate pde sources for node " + sourcesNode, e);
+               }
+       }
+
+       protected void packageSourcesAsPdeSource(Node sourcesNode) {
+               Binary origBinary = null;
+               Binary osgiBinary = null;
+               try {
+                       Session session = sourcesNode.getSession();
+                       Artifact sourcesArtifact = AetherUtils.convertPathToArtifact(sourcesNode.getPath(), null);
+
+                       // read name version from manifest
+                       Artifact osgiArtifact = new DefaultArtifact(sourcesArtifact.getGroupId(), sourcesArtifact.getArtifactId(),
+                                       sourcesArtifact.getExtension(), sourcesArtifact.getVersion());
+                       String osgiPath = MavenConventionsUtils.artifactPath(artifactBasePath, osgiArtifact);
+                       osgiBinary = session.getNode(osgiPath).getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary();
+
+                       NameVersion nameVersion = RepoUtils.readNameVersion(osgiBinary.getStream());
+                       if (nameVersion == null) {
+                               log.warn("Cannot package PDE sources for " + osgiPath + " as it is probably not an OSGi bundle");
+                               return;
+                       }
+
+                       // create PDe sources artifact
+                       Artifact pdeSourceArtifact = new DefaultArtifact(sourcesArtifact.getGroupId(),
+                                       sourcesArtifact.getArtifactId() + ".source", sourcesArtifact.getExtension(),
+                                       sourcesArtifact.getVersion());
+                       String targetSourceParentPath = MavenConventionsUtils.artifactParentPath(artifactBasePath,
+                                       pdeSourceArtifact);
+                       String targetSourceFileName = MavenConventionsUtils.artifactFileName(pdeSourceArtifact);
+                       // String targetSourceJarPath = targetSourceParentPath + '/'
+                       // + targetSourceFileName;
+
+                       Node targetSourceParentNode = JcrUtils.mkfolders(session, targetSourceParentPath);
+                       origBinary = sourcesNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary();
+                       byte[] targetJarBytes = RepoUtils.packageAsPdeSource(origBinary.getStream(), nameVersion);
+                       JcrUtils.copyBytesAsFile(targetSourceParentNode, targetSourceFileName, targetJarBytes);
+
+                       // reindex
+                       // Automagically done via the various listeners or manually
+                       // triggered.
+                       // Node targetSourceJarNode = session.getNode(targetSourceJarPath);
+                       // artifactIndexer.index(targetSourceJarNode);
+                       // jarFileIndexer.index(targetSourceJarNode);
+                       if (log.isTraceEnabled())
+                               log.trace("Created pde source artifact " + pdeSourceArtifact + " from " + sourcesNode);
+
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot add PDE sources for " + sourcesNode, e);
+               } finally {
+                       JcrUtils.closeQuietly(origBinary);
+                       JcrUtils.closeQuietly(osgiBinary);
+               }
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/RepoConstants.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/RepoConstants.java
new file mode 100644 (file)
index 0000000..38f274a
--- /dev/null
@@ -0,0 +1,27 @@
+package org.argeo.slc.repo;
+
+import org.argeo.api.cms.CmsConstants;
+
+/** SLC repository constants */
+public interface RepoConstants {
+       String DEFAULT_JAVA_REPOSITORY_ALIAS = "java";
+       String DEFAULT_JAVA_REPOSITORY_LABEL = "Internal Java Repository";
+
+
+       String DEFAULT_ARTIFACTS_BASE_PATH = "/";
+       String REPO_BASEPATH = "/slc:repo";
+       String PROXIED_REPOSITORIES = REPO_BASEPATH + "/slc:sources";
+       String DISTRIBUTIONS_BASE_PATH = REPO_BASEPATH + "/slc:distributions";
+       String REPOSITORIES_BASE_PATH = REPO_BASEPATH + "/slc:repositories";
+       String DIST_DOWNLOAD_BASEPATH = "/download";
+
+       String BINARIES_ARTIFACT_ID = "binaries";
+       String SOURCES_ARTIFACT_ID = "sources";
+       String SDK_ARTIFACT_ID = "sdk";
+
+       // TODO might exists somewhere else
+       String SLC_CATEGORY_ID = "SLC-Category";
+
+       // TODO find a more generic way
+       String DEFAULT_DEFAULT_WORKSPACE = CmsConstants.SYS_WORKSPACE;
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/RepoService.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/RepoService.java
new file mode 100644 (file)
index 0000000..9b9bc66
--- /dev/null
@@ -0,0 +1,14 @@
+package org.argeo.slc.repo;
+
+import javax.jcr.Session;
+
+/** Start factorisation of the session management using a manager service */
+public interface RepoService {
+
+       /**
+        * Returns a corresponding session given the current context. Caller must
+        * close the session once it has been used
+        */
+       public Session getRemoteSession(String repoNodePath, String uri,
+                       String workspaceName);
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/RepoSync.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/RepoSync.java
new file mode 100644 (file)
index 0000000..567ea36
--- /dev/null
@@ -0,0 +1,589 @@
+package org.argeo.slc.repo;
+
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+import javax.jcr.Binary;
+import javax.jcr.Credentials;
+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.RepositoryFactory;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.query.Query;
+import javax.jcr.query.QueryResult;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.jcr.JcrMonitor;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcException;
+import org.xml.sax.SAXException;
+
+/**
+ * Synchronise workspaces from a remote software repository to the local
+ * repository (Synchronisation in the other direction does not work).
+ * 
+ * Workspaces are retrieved by name given a map that links the source with a
+ * target name. If a target workspace does not exist, it is created. Otherwise
+ * we copy the content of the source workspace into the target one.
+ */
+public class RepoSync implements Runnable {
+       private final static CmsLog log = CmsLog.getLog(RepoSync.class);
+
+       // Centralizes definition of workspaces that must be ignored by the sync.
+       private final static List<String> IGNORED_WKSP_LIST = Arrays.asList("security", "localrepo");
+
+       private final Calendar zero;
+       private Session sourceDefaultSession = null;
+       private Session targetDefaultSession = null;
+
+       private Repository sourceRepository;
+       private Credentials sourceCredentials;
+       private Repository targetRepository;
+       private Credentials targetCredentials;
+
+       // if Repository and Credentials objects are not explicitly set
+       private String sourceRepoUri;
+       private String sourceUsername;
+       private char[] sourcePassword;
+       private String targetRepoUri;
+       private String targetUsername;
+       private char[] targetPassword;
+
+       private RepositoryFactory repositoryFactory;
+
+       private JcrMonitor monitor;
+       private Map<String, String> workspaceMap;
+
+       // TODO fix monitor
+       private Boolean filesOnly = false;
+
+       public RepoSync() {
+               zero = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+               zero.setTimeInMillis(0);
+       }
+
+       /**
+        * 
+        * Shortcut to instantiate a RepoSync with already known repositories and
+        * credentials.
+        * 
+        * @param sourceRepository
+        * @param sourceCredentials
+        * @param targetRepository
+        * @param targetCredentials
+        */
+       public RepoSync(Repository sourceRepository, Credentials sourceCredentials, Repository targetRepository,
+                       Credentials targetCredentials) {
+               this();
+               this.sourceRepository = sourceRepository;
+               this.sourceCredentials = sourceCredentials;
+               this.targetRepository = targetRepository;
+               this.targetCredentials = targetCredentials;
+       }
+
+       public void run() {
+               try {
+                       long begin = System.currentTimeMillis();
+
+                       // Setup
+                       if (sourceRepository == null)
+                               sourceRepository = CmsJcrUtils.getRepositoryByUri(repositoryFactory, sourceRepoUri);
+                       if (sourceCredentials == null && sourceUsername != null)
+                               sourceCredentials = new SimpleCredentials(sourceUsername, sourcePassword);
+                       // FIXME make it more generic
+                       sourceDefaultSession = sourceRepository.login(sourceCredentials, RepoConstants.DEFAULT_DEFAULT_WORKSPACE);
+
+                       if (targetRepository == null)
+                               targetRepository = CmsJcrUtils.getRepositoryByUri(repositoryFactory, targetRepoUri);
+                       if (targetCredentials == null && targetUsername != null)
+                               targetCredentials = new SimpleCredentials(targetUsername, targetPassword);
+                       targetDefaultSession = targetRepository.login(targetCredentials);
+
+                       Map<String, Exception> errors = new HashMap<String, Exception>();
+                       for (String sourceWorkspaceName : sourceDefaultSession.getWorkspace().getAccessibleWorkspaceNames()) {
+                               if (monitor != null && monitor.isCanceled())
+                                       break;
+
+                               if (workspaceMap != null && !workspaceMap.containsKey(sourceWorkspaceName))
+                                       continue;
+                               if (IGNORED_WKSP_LIST.contains(sourceWorkspaceName))
+                                       continue;
+
+                               Session sourceSession = null;
+                               Session targetSession = null;
+                               String targetWorkspaceName = workspaceMap.get(sourceWorkspaceName);
+                               try {
+                                       try {
+                                               targetSession = targetRepository.login(targetCredentials, targetWorkspaceName);
+                                       } catch (NoSuchWorkspaceException e) {
+                                               targetDefaultSession.getWorkspace().createWorkspace(targetWorkspaceName);
+                                               targetSession = targetRepository.login(targetCredentials, targetWorkspaceName);
+                                       }
+                                       sourceSession = sourceRepository.login(sourceCredentials, sourceWorkspaceName);
+                                       syncWorkspace(sourceSession, targetSession);
+                               } catch (Exception e) {
+                                       errors.put("Could not sync workspace " + sourceWorkspaceName, e);
+                                       if (log.isErrorEnabled())
+                                               e.printStackTrace();
+
+                               } finally {
+                                       JcrUtils.logoutQuietly(sourceSession);
+                                       JcrUtils.logoutQuietly(targetSession);
+                               }
+                       }
+
+                       if (monitor != null && monitor.isCanceled())
+                               log.info("Sync has been canceled by user");
+
+                       long duration = (System.currentTimeMillis() - begin) / 1000;// s
+                       log.info("Sync " + sourceRepoUri + " to " + targetRepoUri + " in " + (duration / 60)
+
+                                       + "min " + (duration % 60) + "s");
+
+                       if (errors.size() > 0) {
+                               throw new SlcException("Sync failed " + errors);
+                       }
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot sync " + sourceRepoUri + " to " + targetRepoUri, e);
+               } finally {
+                       JcrUtils.logoutQuietly(sourceDefaultSession);
+                       JcrUtils.logoutQuietly(targetDefaultSession);
+               }
+       }
+
+       private long getNodesNumber(Session session) {
+               if (IGNORED_WKSP_LIST.contains(session.getWorkspace().getName()))
+                       return 0l;
+               try {
+                       Query countQuery = session.getWorkspace().getQueryManager().createQuery(
+                                       "select file from [" + (true ? NodeType.NT_FILE : NodeType.NT_BASE) + "] as file", Query.JCR_SQL2);
+
+                       QueryResult result = countQuery.execute();
+                       Long expectedCount = result.getNodes().getSize();
+                       return expectedCount;
+               } catch (RepositoryException e) {
+                       throw new SlcException("Unexpected error while computing " + "the size of the fetch for workspace "
+                                       + session.getWorkspace().getName(), e);
+               }
+       }
+
+       protected void syncWorkspace(Session sourceSession, Session targetSession) {
+               if (monitor != null) {
+                       monitor.beginTask("Computing fetch size...", -1);
+                       Long totalAmount = getNodesNumber(sourceSession);
+                       monitor.beginTask("Fetch", totalAmount.intValue());
+               }
+
+               try {
+                       String msg = "Synchronizing workspace: " + sourceSession.getWorkspace().getName();
+                       if (monitor != null)
+                               monitor.setTaskName(msg);
+                       if (log.isDebugEnabled())
+                               log.debug(msg);
+
+                       for (NodeIterator it = sourceSession.getRootNode().getNodes(); it.hasNext();) {
+                               Node node = it.nextNode();
+                               if (node.getName().contains(":"))
+                                       continue;
+                               if (node.getName().equals("download"))
+                                       continue;
+                               if (!node.isNodeType(NodeType.NT_HIERARCHY_NODE))
+                                       continue;
+                               syncNode(node, targetSession);
+                       }
+                       // if (filesOnly) {
+                       // JcrUtils.copyFiles(sourceSession.getRootNode(), targetSession.getRootNode(),
+                       // true, monitor);
+                       // } else {
+                       // for (NodeIterator it = sourceSession.getRootNode().getNodes(); it.hasNext();)
+                       // {
+                       // Node node = it.nextNode();
+                       // if (node.getName().equals("jcr:system"))
+                       // continue;
+                       // syncNode(node, targetSession);
+                       // }
+                       // }
+                       if (log.isDebugEnabled())
+                               log.debug("Synced " + sourceSession.getWorkspace().getName());
+               } catch (Exception e) {
+                       e.printStackTrace();
+                       throw new SlcException("Cannot sync " + sourceSession.getWorkspace().getName() + " to "
+                                       + targetSession.getWorkspace().getName(), e);
+               }
+       }
+
+       /** factorizes monitor management */
+       private void updateMonitor(String msg) {
+               updateMonitor(msg, false);
+       }
+
+       protected void syncNode(Node sourceNode, Session targetSession) throws RepositoryException, SAXException {
+               if (filesOnly) {
+                       Node targetNode;
+                       if (targetSession.itemExists(sourceNode.getPath()))
+                               targetNode = targetSession.getNode(sourceNode.getPath());
+                       else
+                               targetNode = JcrUtils.mkdirs(targetSession, sourceNode.getPath(), NodeType.NT_FOLDER);
+                       JcrUtils.copyFiles(sourceNode, targetNode, true, monitor, true);
+                       return;
+               }
+               // Boolean singleLevel = singleLevel(sourceNode);
+               try {
+                       if (monitor != null && monitor.isCanceled()) {
+                               updateMonitor("Fetched has been canceled, " + "process is terminating");
+                               return;
+                       }
+
+                       Node targetParentNode = targetSession.getNode(sourceNode.getParent().getPath());
+                       Node targetNode;
+                       if (monitor != null && sourceNode.isNodeType(NodeType.NT_HIERARCHY_NODE))
+                               monitor.subTask("Process " + sourceNode.getPath());
+
+                       final Boolean isNew;
+                       if (!targetSession.itemExists(sourceNode.getPath())) {
+                               isNew = true;
+                               targetNode = targetParentNode.addNode(sourceNode.getName(), sourceNode.getPrimaryNodeType().getName());
+                       } else {
+                               isNew = false;
+                               targetNode = targetSession.getNode(sourceNode.getPath());
+                               if (!targetNode.getPrimaryNodeType().getName().equals(sourceNode.getPrimaryNodeType().getName()))
+                                       targetNode.setPrimaryType(sourceNode.getPrimaryNodeType().getName());
+                       }
+
+                       // export
+                       // sourceNode.getSession().exportSystemView(sourceNode.getPath(),
+                       // contentHandler, false, singleLevel);
+
+                       // if (singleLevel) {
+                       // if (targetSession.hasPendingChanges()) {
+                       // // updateMonitor(
+                       // // (isNew ? "Added " : "Updated ") + targetNode.getPath(),
+                       // // true);
+                       // if (doSave)
+                       // targetSession.save();
+                       // } else {
+                       // // updateMonitor("Checked " + targetNode.getPath(), false);
+                       // }
+                       // }
+
+                       // mixin and properties
+                       for (NodeType nt : sourceNode.getMixinNodeTypes()) {
+                               if (!targetNode.isNodeType(nt.getName()) && targetNode.canAddMixin(nt.getName()))
+                                       targetNode.addMixin(nt.getName());
+                       }
+                       copyProperties(sourceNode, targetNode);
+
+                       // next level
+                       NodeIterator ni = sourceNode.getNodes();
+                       while (ni != null && ni.hasNext()) {
+                               Node sourceChild = ni.nextNode();
+                               syncNode(sourceChild, targetSession);
+                       }
+
+                       copyTimestamps(sourceNode, targetNode);
+
+                       if (sourceNode.isNodeType(NodeType.NT_HIERARCHY_NODE)) {
+                               if (targetSession.hasPendingChanges()) {
+                                       if (sourceNode.isNodeType(NodeType.NT_FILE))
+                                               updateMonitor((isNew ? "Added " : "Updated ") + targetNode.getPath(), true);
+                                       // if (doSave)
+                                       targetSession.save();
+                               } else {
+                                       if (sourceNode.isNodeType(NodeType.NT_FILE))
+                                               updateMonitor("Checked " + targetNode.getPath(), false);
+                               }
+                       }
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot sync source node " + sourceNode, e);
+               }
+       }
+
+       private void copyTimestamps(Node sourceNode, Node targetNode) throws RepositoryException {
+               if (sourceNode.getDefinition().isProtected())
+                       return;
+               if (targetNode.getDefinition().isProtected())
+                       return;
+               copyTimestamp(sourceNode, targetNode, Property.JCR_CREATED);
+               copyTimestamp(sourceNode, targetNode, Property.JCR_CREATED_BY);
+               copyTimestamp(sourceNode, targetNode, Property.JCR_LAST_MODIFIED);
+               copyTimestamp(sourceNode, targetNode, Property.JCR_LAST_MODIFIED_BY);
+       }
+
+       private void copyTimestamp(Node sourceNode, Node targetNode, String property) throws RepositoryException {
+               if (sourceNode.hasProperty(property)) {
+                       Property p = sourceNode.getProperty(property);
+                       if (p.getDefinition().isProtected())
+                               return;
+                       if (targetNode.hasProperty(property)
+                                       && targetNode.getProperty(property).getValue().equals(sourceNode.getProperty(property).getValue()))
+                               return;
+                       targetNode.setProperty(property, sourceNode.getProperty(property).getValue());
+               }
+       }
+
+       private void copyProperties(Node sourceNode, Node targetNode) throws RepositoryException {
+               properties: for (PropertyIterator pi = sourceNode.getProperties(); pi.hasNext();) {
+                       Property p = pi.nextProperty();
+                       if (p.getDefinition().isProtected())
+                               continue properties;
+                       if (p.getName().equals(Property.JCR_CREATED) || p.getName().equals(Property.JCR_CREATED_BY)
+                                       || p.getName().equals(Property.JCR_LAST_MODIFIED)
+                                       || p.getName().equals(Property.JCR_LAST_MODIFIED_BY))
+                               continue properties;
+
+                       if (p.getType() == PropertyType.BINARY) {
+                               copyBinary(p, targetNode);
+                       } else {
+
+                               if (p.isMultiple()) {
+                                       if (!targetNode.hasProperty(p.getName())
+                                                       || !Arrays.equals(targetNode.getProperty(p.getName()).getValues(), p.getValues()))
+                                               targetNode.setProperty(p.getName(), p.getValues());
+                               } else {
+                                       if (!targetNode.hasProperty(p.getName())
+                                                       || !targetNode.getProperty(p.getName()).getValue().equals(p.getValue()))
+                                               targetNode.setProperty(p.getName(), p.getValue());
+                               }
+                       }
+               }
+       }
+
+       private static void copyBinary(Property p, Node targetNode) throws RepositoryException {
+               InputStream in = null;
+               Binary sourceBinary = null;
+               Binary targetBinary = null;
+               try {
+                       sourceBinary = p.getBinary();
+                       if (targetNode.hasProperty(p.getName()))
+                               targetBinary = targetNode.getProperty(p.getName()).getBinary();
+
+                       // optim FIXME make it more configurable
+                       if (targetBinary != null)
+                               if (sourceBinary.getSize() == targetBinary.getSize()) {
+                                       if (log.isTraceEnabled())
+                                               log.trace("Skipped " + p.getPath());
+                                       return;
+                               }
+
+                       in = sourceBinary.getStream();
+                       targetBinary = targetNode.getSession().getValueFactory().createBinary(in);
+                       targetNode.setProperty(p.getName(), targetBinary);
+               } catch (Exception e) {
+                       throw new SlcException("Could not transfer " + p, e);
+               } finally {
+                       IOUtils.closeQuietly(in);
+                       JcrUtils.closeQuietly(sourceBinary);
+                       JcrUtils.closeQuietly(targetBinary);
+               }
+       }
+
+       /** factorizes monitor management */
+       private void updateMonitor(String msg, Boolean doLog) {
+               if (doLog && log.isDebugEnabled())
+                       log.debug(msg);
+               if (monitor != null) {
+                       monitor.worked(1);
+                       monitor.subTask(msg);
+               }
+       }
+
+       // private void syncNode_old(Node sourceNode, Node targetParentNode)
+       // throws RepositoryException, SAXException {
+       //
+       // // enable cancelation of the current fetch process
+       // // fxme insure the repository stays in a stable state
+       // if (monitor != null && monitor.isCanceled()) {
+       // updateMonitor("Fetched has been canceled, "
+       // + "process is terminating");
+       // return;
+       // }
+       //
+       // Boolean noRecurse = singleLevel(sourceNode);
+       // Calendar sourceLastModified = null;
+       // if (sourceNode.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
+       // sourceLastModified = sourceNode.getProperty(
+       // Property.JCR_LAST_MODIFIED).getDate();
+       // }
+       //
+       // if (sourceNode.getDefinition().isProtected())
+       // log.warn(sourceNode + " is protected.");
+       //
+       // if (!targetParentNode.hasNode(sourceNode.getName())) {
+       // String msg = "Adding " + sourceNode.getPath();
+       // updateMonitor(msg);
+       // if (log.isDebugEnabled())
+       // log.debug(msg);
+       // ContentHandler contentHandler = targetParentNode
+       // .getSession()
+       // .getWorkspace()
+       // .getImportContentHandler(targetParentNode.getPath(),
+       // ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW);
+       // sourceNode.getSession().exportSystemView(sourceNode.getPath(),
+       // contentHandler, false, noRecurse);
+       // } else {
+       // Node targetNode = targetParentNode.getNode(sourceNode.getName());
+       // if (sourceLastModified != null) {
+       // Calendar targetLastModified = null;
+       // if (targetNode.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
+       // targetLastModified = targetNode.getProperty(
+       // Property.JCR_LAST_MODIFIED).getDate();
+       // }
+       //
+       // if (targetLastModified == null
+       // || targetLastModified.before(sourceLastModified)) {
+       // String msg = "Updating " + targetNode.getPath();
+       // updateMonitor(msg);
+       // if (log.isDebugEnabled())
+       // log.debug(msg);
+       // ContentHandler contentHandler = targetParentNode
+       // .getSession()
+       // .getWorkspace()
+       // .getImportContentHandler(
+       // targetParentNode.getPath(),
+       // ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
+       // sourceNode.getSession().exportSystemView(
+       // sourceNode.getPath(), contentHandler, false,
+       // noRecurse);
+       // } else {
+       // String msg = "Skipped up to date " + targetNode.getPath();
+       // updateMonitor(msg);
+       // if (log.isDebugEnabled())
+       // log.debug(msg);
+       // return;
+       // }
+       // }
+       // }
+       //
+       // if (noRecurse) {
+       // // recurse
+       // Node targetNode = targetParentNode.getNode(sourceNode.getName());
+       // if (sourceLastModified != null) {
+       // Calendar zero = new GregorianCalendar();
+       // zero.setTimeInMillis(0);
+       // targetNode.setProperty(Property.JCR_LAST_MODIFIED, zero);
+       // targetNode.getSession().save();
+       // }
+       //
+       // for (NodeIterator it = sourceNode.getNodes(); it.hasNext();) {
+       // syncNode_old(it.nextNode(), targetNode);
+       // }
+       //
+       // if (sourceLastModified != null) {
+       // targetNode.setProperty(Property.JCR_LAST_MODIFIED,
+       // sourceLastModified);
+       // targetNode.getSession().save();
+       // }
+       // }
+       // }
+
+       protected Boolean singleLevel(Node sourceNode) throws RepositoryException {
+               if (sourceNode.isNodeType(NodeType.NT_FILE))
+                       return false;
+               return true;
+       }
+
+       /**
+        * Synchronises only one workspace, retrieved by name without changing its name.
+        */
+       public void setSourceWksp(String sourceWksp) {
+               if (sourceWksp != null && !sourceWksp.trim().equals("")) {
+                       Map<String, String> map = new HashMap<String, String>();
+                       map.put(sourceWksp, sourceWksp);
+                       setWkspMap(map);
+               }
+       }
+
+       /**
+        * Synchronises a map of workspaces that will be retrieved by name. If the
+        * target name is not defined (eg null or an empty string) for a given source
+        * workspace, we use the source name as target name.
+        */
+       public void setWkspMap(Map<String, String> workspaceMap) {
+               // clean the list to ease later use
+               this.workspaceMap = new HashMap<String, String>();
+               if (workspaceMap != null) {
+                       workspaceNames: for (String srcName : workspaceMap.keySet()) {
+                               String targetName = workspaceMap.get(srcName);
+
+                               // Sanity check
+                               if (srcName.trim().equals(""))
+                                       continue workspaceNames;
+                               if (targetName == null || "".equals(targetName.trim()))
+                                       targetName = srcName;
+                               this.workspaceMap.put(srcName, targetName);
+                       }
+               }
+               // clean the map to ease later use
+               if (this.workspaceMap.size() == 0)
+                       this.workspaceMap = null;
+       }
+
+       public void setMonitor(JcrMonitor monitor) {
+               this.monitor = monitor;
+       }
+
+       public void setRepositoryFactory(RepositoryFactory repositoryFactory) {
+               this.repositoryFactory = repositoryFactory;
+       }
+
+       public void setSourceRepoUri(String sourceRepoUri) {
+               this.sourceRepoUri = sourceRepoUri;
+       }
+
+       public void setSourceUsername(String sourceUsername) {
+               this.sourceUsername = sourceUsername;
+       }
+
+       public void setSourcePassword(char[] sourcePassword) {
+               this.sourcePassword = sourcePassword;
+       }
+
+       public void setTargetRepoUri(String targetRepoUri) {
+               this.targetRepoUri = targetRepoUri;
+       }
+
+       public void setTargetUsername(String targetUsername) {
+               this.targetUsername = targetUsername;
+       }
+
+       public void setTargetPassword(char[] targetPassword) {
+               this.targetPassword = targetPassword;
+       }
+
+       public void setSourceRepository(Repository sourceRepository) {
+               this.sourceRepository = sourceRepository;
+       }
+
+       public void setSourceCredentials(Credentials sourceCredentials) {
+               this.sourceCredentials = sourceCredentials;
+       }
+
+       public void setTargetRepository(Repository targetRepository) {
+               this.targetRepository = targetRepository;
+       }
+
+       public void setTargetCredentials(Credentials targetCredentials) {
+               this.targetCredentials = targetCredentials;
+       }
+
+       public void setFilesOnly(Boolean filesOnly) {
+               this.filesOnly = filesOnly;
+       }
+
+}
\ No newline at end of file
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/RepoUtils.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/RepoUtils.java
new file mode 100644 (file)
index 0000000..22fe6b0
--- /dev/null
@@ -0,0 +1,633 @@
+package org.argeo.slc.repo;
+
+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.util.Enumeration;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.TreeSet;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarInputStream;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.zip.ZipInputStream;
+
+import javax.jcr.Credentials;
+import javax.jcr.GuestCredentials;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.api.cms.keyring.Keyring;
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.ArgeoTypes;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.jcr.JcrMonitor;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.DefaultNameVersion;
+import org.argeo.slc.NameVersion;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.argeo.slc.repo.maven.ArtifactIdComparator;
+import org.argeo.slc.repo.maven.MavenConventionsUtils;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.osgi.framework.Constants;
+
+/** Utilities around repo */
+public class RepoUtils implements ArgeoNames, SlcNames {
+       private final static CmsLog log = CmsLog.getLog(RepoUtils.class);
+
+       /** Packages a regular sources jar as PDE source. */
+       public static void packagesAsPdeSource(File sourceFile,
+                       NameVersion nameVersion, OutputStream out) throws IOException {
+               if (isAlreadyPdeSource(sourceFile)) {
+                       FileInputStream in = new FileInputStream(sourceFile);
+                       IOUtils.copy(in, out);
+                       IOUtils.closeQuietly(in);
+               } else {
+                       String sourceSymbolicName = nameVersion.getName() + ".source";
+
+                       Manifest sourceManifest = null;
+                       sourceManifest = new Manifest();
+                       sourceManifest.getMainAttributes().put(
+                                       Attributes.Name.MANIFEST_VERSION, "1.0");
+                       sourceManifest.getMainAttributes().putValue("Bundle-SymbolicName",
+                                       sourceSymbolicName);
+                       sourceManifest.getMainAttributes().putValue("Bundle-Version",
+                                       nameVersion.getVersion());
+                       sourceManifest.getMainAttributes().putValue(
+                                       "Eclipse-SourceBundle",
+                                       nameVersion.getName() + ";version="
+                                                       + nameVersion.getVersion());
+                       copyJar(sourceFile, out, sourceManifest);
+               }
+       }
+
+       public static byte[] packageAsPdeSource(InputStream sourceJar,
+                       NameVersion nameVersion) {
+               String sourceSymbolicName = nameVersion.getName() + ".source";
+
+               Manifest sourceManifest = null;
+               sourceManifest = new Manifest();
+               sourceManifest.getMainAttributes().put(
+                               Attributes.Name.MANIFEST_VERSION, "1.0");
+               sourceManifest.getMainAttributes().putValue("Bundle-SymbolicName",
+                               sourceSymbolicName);
+               sourceManifest.getMainAttributes().putValue("Bundle-Version",
+                               nameVersion.getVersion());
+               sourceManifest.getMainAttributes().putValue("Eclipse-SourceBundle",
+                               nameVersion.getName() + ";version=" + nameVersion.getVersion());
+
+               return modifyManifest(sourceJar, sourceManifest);
+       }
+
+       /**
+        * Check whether the file as already been packaged as PDE source, in order
+        * not to mess with Jar signing
+        */
+       private static boolean isAlreadyPdeSource(File sourceFile) {
+               JarInputStream jarInputStream = null;
+
+               try {
+                       jarInputStream = new JarInputStream(new FileInputStream(sourceFile));
+
+                       Manifest manifest = jarInputStream.getManifest();
+                       Iterator<?> it = manifest.getMainAttributes().keySet().iterator();
+                       boolean res = false;
+                       // containsKey() does not work, iterating...
+                       while (it.hasNext())
+                               if (it.next().toString().equals("Eclipse-SourceBundle")) {
+                                       res = true;
+                                       break;
+                               }
+                       // boolean res = manifest.getMainAttributes().get(
+                       // "Eclipse-SourceBundle") != null;
+                       if (res)
+                               log.info(sourceFile + " is already a PDE source");
+                       return res;
+               } catch (Exception e) {
+                       // probably not a jar, skipping
+                       if (log.isDebugEnabled())
+                               log.debug("Skipping " + sourceFile + " because of "
+                                               + e.getMessage());
+                       return false;
+               } finally {
+                       IOUtils.closeQuietly(jarInputStream);
+               }
+       }
+
+       /**
+        * Copy a jar, replacing its manifest with the provided one
+        * 
+        * @param manifest
+        *            can be null
+        */
+       private static void copyJar(File source, OutputStream out, Manifest manifest)
+                       throws IOException {
+               JarFile sourceJar = null;
+               JarOutputStream output = null;
+               try {
+                       output = manifest != null ? new JarOutputStream(out, manifest)
+                                       : new JarOutputStream(out);
+                       sourceJar = new JarFile(source);
+
+                       entries: for (Enumeration<?> entries = sourceJar.entries(); entries
+                                       .hasMoreElements();) {
+                               JarEntry entry = (JarEntry) entries.nextElement();
+                               if (manifest != null
+                                               && entry.getName().equals("META-INF/MANIFEST.MF"))
+                                       continue entries;
+
+                               InputStream entryStream = sourceJar.getInputStream(entry);
+                               JarEntry newEntry = new JarEntry(entry.getName());
+                               // newEntry.setMethod(JarEntry.DEFLATED);
+                               output.putNextEntry(newEntry);
+                               IOUtils.copy(entryStream, output);
+                       }
+               } finally {
+                       IOUtils.closeQuietly(output);
+                       try {
+                               if (sourceJar != null)
+                                       sourceJar.close();
+                       } catch (IOException e) {
+                               // silent
+                       }
+               }
+       }
+
+       /** Copy a jar changing onlythe manifest */
+       public static void copyJar(InputStream in, OutputStream out,
+                       Manifest manifest) {
+               JarInputStream jarIn = null;
+               JarOutputStream jarOut = null;
+               try {
+                       jarIn = new JarInputStream(in);
+                       jarOut = new JarOutputStream(out, manifest);
+                       JarEntry jarEntry = null;
+                       while ((jarEntry = jarIn.getNextJarEntry()) != null) {
+                               if (!jarEntry.getName().equals("META-INF/MANIFEST.MF")) {
+                                       JarEntry newJarEntry = new JarEntry(jarEntry.getName());
+                                       jarOut.putNextEntry(newJarEntry);
+                                       IOUtils.copy(jarIn, jarOut);
+                                       jarIn.closeEntry();
+                                       jarOut.closeEntry();
+                               }
+                       }
+               } catch (IOException e) {
+                       throw new SlcException("Could not copy jar with MANIFEST "
+                                       + manifest.getMainAttributes(), e);
+               } finally {
+                       if (!(in instanceof ZipInputStream))
+                               IOUtils.closeQuietly(jarIn);
+                       IOUtils.closeQuietly(jarOut);
+               }
+       }
+
+       /** Reads a jar file, modify its manifest */
+       public static byte[] modifyManifest(InputStream in, Manifest manifest) {
+               ByteArrayOutputStream out = new ByteArrayOutputStream(200 * 1024);
+               try {
+                       copyJar(in, out, manifest);
+                       return out.toByteArray();
+               } finally {
+                       IOUtils.closeQuietly(out);
+               }
+       }
+
+       /** Read the OSGi {@link NameVersion} */
+       public static NameVersion readNameVersion(Artifact artifact) {
+               File artifactFile = artifact.getFile();
+               if (artifact.getExtension().equals("pom")) {
+                       // hack to process jars which weirdly appear as POMs
+                       File jarFile = new File(artifactFile.getParentFile(),
+                                       FilenameUtils.getBaseName(artifactFile.getPath()) + ".jar");
+                       if (jarFile.exists()) {
+                               log.warn("Use " + jarFile + " instead of " + artifactFile
+                                               + " for " + artifact);
+                               artifactFile = jarFile;
+                       }
+               }
+               return readNameVersion(artifactFile);
+       }
+
+       /** Read the OSGi {@link NameVersion} */
+       public static NameVersion readNameVersion(File artifactFile) {
+               try {
+                       return readNameVersion(new FileInputStream(artifactFile));
+               } catch (Exception e) {
+                       // probably not a jar, skipping
+                       if (log.isDebugEnabled()) {
+                               log.debug("Skipping " + artifactFile + " because of " + e);
+                               // e.printStackTrace();
+                       }
+               }
+               return null;
+       }
+
+       /** Read the OSGi {@link NameVersion} */
+       public static NameVersion readNameVersion(InputStream in) {
+               JarInputStream jarInputStream = null;
+               try {
+                       jarInputStream = new JarInputStream(in);
+                       return readNameVersion(jarInputStream.getManifest());
+               } catch (Exception e) {
+                       // probably not a jar, skipping
+                       if (log.isDebugEnabled()) {
+                               log.debug("Skipping because of " + e);
+                       }
+               } finally {
+                       IOUtils.closeQuietly(jarInputStream);
+               }
+               return null;
+       }
+
+       /** Read the OSGi {@link NameVersion} */
+       public static NameVersion readNameVersion(Manifest manifest) {
+               DefaultNameVersion nameVersion = new DefaultNameVersion();
+               nameVersion.setName(manifest.getMainAttributes().getValue(
+                               Constants.BUNDLE_SYMBOLICNAME));
+
+               // Skip additional specs such as
+               // ; singleton:=true
+               if (nameVersion.getName().indexOf(';') > -1) {
+                       nameVersion
+                                       .setName(new StringTokenizer(nameVersion.getName(), " ;")
+                                                       .nextToken());
+               }
+
+               nameVersion.setVersion(manifest.getMainAttributes().getValue(
+                               Constants.BUNDLE_VERSION));
+
+               return nameVersion;
+       }
+
+       /*
+        * DATA MODEL
+        */
+       /** The artifact described by this node */
+       public static Artifact asArtifact(Node node) throws RepositoryException {
+               if (node.isNodeType(SlcTypes.SLC_ARTIFACT_VERSION_BASE)) {
+                       // FIXME update data model to store packaging at this level
+                       String extension = "jar";
+                       return new DefaultArtifact(node.getProperty(SLC_GROUP_ID)
+                                       .getString(),
+                                       node.getProperty(SLC_ARTIFACT_ID).getString(), extension,
+                                       node.getProperty(SLC_ARTIFACT_VERSION).getString());
+               } else if (node.isNodeType(SlcTypes.SLC_ARTIFACT)) {
+                       return new DefaultArtifact(node.getProperty(SLC_GROUP_ID)
+                                       .getString(),
+                                       node.getProperty(SLC_ARTIFACT_ID).getString(), node
+                                                       .getProperty(SLC_ARTIFACT_CLASSIFIER).getString(),
+                                       node.getProperty(SLC_ARTIFACT_EXTENSION).getString(), node
+                                                       .getProperty(SLC_ARTIFACT_VERSION).getString());
+               } else if (node.isNodeType(SlcTypes.SLC_MODULE_COORDINATES)) {
+                       return new DefaultArtifact(node.getProperty(SLC_CATEGORY)
+                                       .getString(), node.getProperty(SLC_NAME).getString(),
+                                       "jar", node.getProperty(SLC_VERSION).getString());
+               } else {
+                       throw new SlcException("Unsupported node type for " + node);
+               }
+       }
+
+       /**
+        * The path to the PDE source related to this artifact (or artifact version
+        * base). There may or there may not be a node at this location (the
+        * returned path will typically be used to test whether PDE sources are
+        * attached to this artifact).
+        */
+       public static String relatedPdeSourcePath(String artifactBasePath,
+                       Node artifactNode) throws RepositoryException {
+               Artifact artifact = asArtifact(artifactNode);
+               Artifact pdeSourceArtifact = new DefaultArtifact(artifact.getGroupId(),
+                               artifact.getArtifactId() + ".source", artifact.getExtension(),
+                               artifact.getVersion());
+               return MavenConventionsUtils.artifactPath(artifactBasePath,
+                               pdeSourceArtifact);
+       }
+
+       /**
+        * Copy this bytes array as an artifact, relative to the root of the
+        * repository (typically the workspace root node)
+        */
+       public static Node copyBytesAsArtifact(Node artifactsBase,
+                       Artifact artifact, byte[] bytes) throws RepositoryException {
+               String parentPath = MavenConventionsUtils.artifactParentPath(
+                               artifactsBase.getPath(), artifact);
+               Node folderNode = JcrUtils.mkfolders(artifactsBase.getSession(),
+                               parentPath);
+               return JcrUtils.copyBytesAsFile(folderNode,
+                               MavenConventionsUtils.artifactFileName(artifact), bytes);
+       }
+
+       private RepoUtils() {
+       }
+
+       /** If a source return the base bundle name, does not change otherwise */
+       public static String extractBundleNameFromSourceName(String sourceBundleName) {
+               if (sourceBundleName.endsWith(".source"))
+                       return sourceBundleName.substring(0, sourceBundleName.length()
+                                       - ".source".length());
+               else
+                       return sourceBundleName;
+       }
+
+       /*
+        * SOFTWARE REPOSITORIES
+        */
+
+       /** Retrieve repository based on information in the repo node */
+       public static Repository getRepository(RepositoryFactory repositoryFactory,
+                       Keyring keyring, Node repoNode) {
+               try {
+                       Repository repository;
+                       if (repoNode.isNodeType(ArgeoTypes.ARGEO_REMOTE_REPOSITORY)) {
+                               String uri = repoNode.getProperty(ARGEO_URI).getString();
+                               if (uri.startsWith("http")) {// http, https
+                                       repository = CmsJcrUtils.getRepositoryByUri(
+                                                       repositoryFactory, uri);
+                               } else if (uri.startsWith("vm:")) {// alias
+                                       repository = CmsJcrUtils.getRepositoryByUri(
+                                                       repositoryFactory, uri);
+                               } else {
+                                       throw new SlcException("Unsupported repository uri " + uri);
+                               }
+                               return repository;
+                       } else {
+                               throw new SlcException("Unsupported node type " + repoNode);
+                       }
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot connect to repository " + repoNode,
+                                       e);
+               }
+       }
+
+       /**
+        * Reads credentials from node, using keyring if there is a password. Can
+        * return null if no credentials needed (local repo) at all, but returns
+        * {@link GuestCredentials} if user id is 'anonymous' .
+        */
+       public static Credentials getRepositoryCredentials(Keyring keyring,
+                       Node repoNode) {
+               try {
+                       if (repoNode.isNodeType(ArgeoTypes.ARGEO_REMOTE_REPOSITORY)) {
+                               if (!repoNode.hasProperty(ARGEO_USER_ID))
+                                       return null;
+
+                               String userId = repoNode.getProperty(ARGEO_USER_ID).getString();
+                               if (userId.equals("anonymous"))// FIXME hardcoded userId
+                                       return new GuestCredentials();
+                               char[] password = keyring.getAsChars(repoNode.getPath() + '/'
+                                               + ARGEO_PASSWORD);
+                               Credentials credentials = new SimpleCredentials(userId,
+                                               password);
+                               return credentials;
+                       } else {
+                               throw new SlcException("Unsupported node type " + repoNode);
+                       }
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot connect to repository " + repoNode,
+                                       e);
+               }
+       }
+
+       /**
+        * Shortcut to retrieve a session given variable information: Handle the
+        * case where we only have an URI of the repository, that we want to connect
+        * as anonymous or the case of a identified connection to a local or remote
+        * repository.
+        * 
+        * Callers must close the session once it has been used
+        */
+       public static Session getRemoteSession(RepositoryFactory repositoryFactory,
+                       Keyring keyring, Node repoNode, String uri, String workspaceName) {
+               try {
+                       if (repoNode == null && uri == null)
+                               throw new SlcException(
+                                               "At least one of repoNode and uri must be defined");
+                       Repository currRepo = null;
+                       Credentials credentials = null;
+                       // Anonymous URI only workspace
+                       if (repoNode == null)
+                               // Anonymous
+                               currRepo = CmsJcrUtils.getRepositoryByUri(repositoryFactory, uri);
+                       else {
+                               currRepo = RepoUtils.getRepository(repositoryFactory, keyring,
+                                               repoNode);
+                               credentials = RepoUtils.getRepositoryCredentials(keyring,
+                                               repoNode);
+                       }
+                       return currRepo.login(credentials, workspaceName);
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot connect to workspace "
+                                       + workspaceName + " of repository " + repoNode
+                                       + " with URI " + uri, e);
+               }
+       }
+
+       /**
+        * Shortcut to retrieve a session on a remote Jrc Repository from
+        * information stored in a local argeo node or from an URI: Handle the case
+        * where we only have an URI of the repository, that we want to connect as
+        * anonymous or the case of a identified connection to a local or remote
+        * repository.
+        * 
+        * Callers must close the session once it has been used
+        */
+       public static Session getRemoteSession(RepositoryFactory repositoryFactory,
+                       Keyring keyring, Repository localRepository, String repoNodePath,
+                       String uri, String workspaceName) {
+               Session localSession = null;
+               Node repoNode = null;
+               try {
+                       localSession = localRepository.login();
+                       if (repoNodePath != null && localSession.nodeExists(repoNodePath))
+                               repoNode = localSession.getNode(repoNodePath);
+
+                       return RepoUtils.getRemoteSession(repositoryFactory, keyring,
+                                       repoNode, uri, workspaceName);
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot log to workspace " + workspaceName
+                                       + " for repo defined in " + repoNodePath, e);
+               } finally {
+                       JcrUtils.logoutQuietly(localSession);
+               }
+       }
+
+       /**
+        * Write group indexes: 'binaries' lists all bundles and their versions,
+        * 'sources' list their sources, and 'sdk' aggregates both.
+        */
+       public static void writeGroupIndexes(Session session,
+                       String artifactBasePath, String groupId, String version,
+                       Set<Artifact> binaries, Set<Artifact> sources) {
+               try {
+                       Set<Artifact> indexes = new TreeSet<Artifact>(
+                                       new ArtifactIdComparator());
+                       Artifact binariesArtifact = writeIndex(session, artifactBasePath,
+                                       groupId, RepoConstants.BINARIES_ARTIFACT_ID, version,
+                                       binaries);
+                       indexes.add(binariesArtifact);
+                       if (sources != null) {
+                               Artifact sourcesArtifact = writeIndex(session,
+                                               artifactBasePath, groupId,
+                                               RepoConstants.SOURCES_ARTIFACT_ID, version, sources);
+                               indexes.add(sourcesArtifact);
+                       }
+                       // sdk
+                       writeIndex(session, artifactBasePath, groupId,
+                                       RepoConstants.SDK_ARTIFACT_ID, version, indexes);
+                       session.save();
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot write indexes for group " + groupId,
+                                       e);
+               }
+       }
+
+       /** Write a group index. */
+       private static Artifact writeIndex(Session session,
+                       String artifactBasePath, String groupId, String artifactId,
+                       String version, Set<Artifact> artifacts) throws RepositoryException {
+               Artifact artifact = new DefaultArtifact(groupId, artifactId, "pom",
+                               version);
+               String pom = MavenConventionsUtils.artifactsAsDependencyPom(artifact,
+                               artifacts, null);
+               Node node = RepoUtils.copyBytesAsArtifact(
+                               session.getNode(artifactBasePath), artifact, pom.getBytes());
+               addMavenChecksums(node);
+               return artifact;
+       }
+
+       /** Add files containing the SHA-1 and MD5 checksums. */
+       public static void addMavenChecksums(Node node) throws RepositoryException {
+               // TODO optimize
+               String sha = JcrUtils.checksumFile(node, "SHA-1");
+               JcrUtils.copyBytesAsFile(node.getParent(), node.getName() + ".sha1",
+                               sha.getBytes());
+               String md5 = JcrUtils.checksumFile(node, "MD5");
+               JcrUtils.copyBytesAsFile(node.getParent(), node.getName() + ".md5",
+                               md5.getBytes());
+       }
+
+       /**
+        * Custom copy since the one in commons does not fit the needs when copying
+        * a workspace completely.
+        */
+       public static void copy(Node fromNode, Node toNode) {
+               copy(fromNode, toNode, null);
+       }
+
+       public static void copy(Node fromNode, Node toNode, JcrMonitor monitor) {
+               try {
+                       String fromPath = fromNode.getPath();
+                       if (monitor != null)
+                               monitor.subTask("copying node :" + fromPath);
+                       if (log.isDebugEnabled())
+                               log.debug("copy node :" + fromPath);
+
+                       // FIXME : small hack to enable specific workspace copy
+                       if (fromNode.isNodeType("rep:ACL")
+                                       || fromNode.isNodeType("rep:system")) {
+                               if (log.isTraceEnabled())
+                                       log.trace("node " + fromNode + " skipped");
+                               return;
+                       }
+
+                       // add mixins
+                       for (NodeType mixinType : fromNode.getMixinNodeTypes()) {
+                               toNode.addMixin(mixinType.getName());
+                       }
+
+                       // Double check
+                       for (NodeType mixinType : toNode.getMixinNodeTypes()) {
+                               if (log.isDebugEnabled())
+                                       log.debug(mixinType.getName());
+                       }
+
+                       // process properties
+                       PropertyIterator pit = fromNode.getProperties();
+                       properties: while (pit.hasNext()) {
+                               Property fromProperty = pit.nextProperty();
+                               String propName = fromProperty.getName();
+                               try {
+                                       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());
+                                       }
+                               } catch (RepositoryException e) {
+                                       throw new SlcException("Cannot property " + propName, e);
+                               }
+                       }
+
+                       // recursively process children nodes
+                       NodeIterator nit = fromNode.getNodes();
+                       while (nit.hasNext()) {
+                               Node fromChild = nit.nextNode();
+                               Integer index = fromChild.getIndex();
+                               String nodeRelPath = fromChild.getName() + "[" + index + "]";
+                               Node toChild;
+                               if (toNode.hasNode(nodeRelPath))
+                                       toChild = toNode.getNode(nodeRelPath);
+                               else
+                                       toChild = toNode.addNode(fromChild.getName(), fromChild
+                                                       .getPrimaryNodeType().getName());
+                               copy(fromChild, toChild);
+                       }
+
+                       // update jcr:lastModified and jcr:lastModifiedBy in toNode in
+                       // case
+                       // they existed
+                       if (!toNode.getDefinition().isProtected()
+                                       && toNode.isNodeType(NodeType.MIX_LAST_MODIFIED))
+                               JcrUtils.updateLastModified(toNode);
+
+                       // Workaround to reduce session size: artifact is a saveable
+                       // unity
+                       if (toNode.isNodeType(SlcTypes.SLC_ARTIFACT))
+                               toNode.getSession().save();
+
+                       if (monitor != null)
+                               monitor.worked(1);
+
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot copy " + fromNode + " to " + toNode, e);
+               }
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/RpmRepoManager.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/RpmRepoManager.java
new file mode 100644 (file)
index 0000000..7535468
--- /dev/null
@@ -0,0 +1,5 @@
+package org.argeo.slc.repo;
+
+public interface RpmRepoManager {
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/SlcRepoManager.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/SlcRepoManager.java
new file mode 100644 (file)
index 0000000..a0ba8e0
--- /dev/null
@@ -0,0 +1,10 @@
+package org.argeo.slc.repo;
+
+/** Coordinator of the various type of repository (Java, RPM, etc.) */
+public interface SlcRepoManager {
+       /** @return null if Java not supported. */
+       public JavaRepoManager getJavaRepoManager();
+
+       /** @return null if RPM not supported. */
+       public RpmRepoManager getRpmRepoManager();
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/core/AbstractJcrRepoManager.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/core/AbstractJcrRepoManager.java
new file mode 100644 (file)
index 0000000..8099d7d
--- /dev/null
@@ -0,0 +1,99 @@
+package org.argeo.slc.repo.core;
+
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.jcr.NoSuchWorkspaceException;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcConstants;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.repo.NodeIndexer;
+
+/** Generic operations on a JCR-based repo. */
+abstract class AbstractJcrRepoManager {
+       private final static CmsLog log = CmsLog.getLog(AbstractJcrRepoManager.class);
+       private String securityWorkspace = "security";
+
+       private Repository jcrRepository;
+       private Session adminSession;
+       private List<NodeIndexer> nodeIndexers;
+
+       // registries
+       private Map<String, Session> workspaceSessions = new TreeMap<String, Session>();
+       private Map<String, WorkspaceIndexer> workspaceIndexers = new TreeMap<String, WorkspaceIndexer>();
+
+       public void init() {
+               try {
+                       adminSession = jcrRepository.login();
+                       String[] workspaceNames = adminSession.getWorkspace().getAccessibleWorkspaceNames();
+                       for (String workspaceName : workspaceNames) {
+                               if (workspaceName.equals(securityWorkspace))
+                                       continue;
+                               if (workspaceName.equals(adminSession.getWorkspace().getName()))
+                                       continue;
+                               workspaceInit(workspaceName);
+                       }
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot initialize repo manager", e);
+               }
+       }
+
+       public void destroy() {
+               for (String key : workspaceIndexers.keySet()) {
+                       workspaceIndexers.get(key).close();
+               }
+
+               for (String key : workspaceSessions.keySet()) {
+                       JcrUtils.logoutQuietly(workspaceSessions.get(key));
+               }
+               JcrUtils.logoutQuietly(adminSession);
+       }
+
+       public void createWorkspace(String workspaceName) {
+               try {
+                       try {
+                               jcrRepository.login(workspaceName);
+                               throw new SlcException("Workspace " + workspaceName + " exists already.");
+                       } catch (NoSuchWorkspaceException e) {
+                               // try to create workspace
+                               adminSession.getWorkspace().createWorkspace(workspaceName);
+                               workspaceInit(workspaceName);
+                       }
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot create workspace " + workspaceName, e);
+               }
+       }
+
+       protected void workspaceInit(String workspaceName) {
+               Session workspaceAdminSession = null;
+               try {
+                       workspaceAdminSession = jcrRepository.login(workspaceName);
+                       workspaceSessions.put(workspaceName, adminSession);
+                       JcrUtils.addPrivilege(workspaceAdminSession, "/", SlcConstants.ROLE_SLC, "jcr:all");
+                       WorkspaceIndexer workspaceIndexer = new WorkspaceIndexer(workspaceAdminSession, nodeIndexers);
+                       workspaceIndexers.put(workspaceName, workspaceIndexer);
+               } catch (RepositoryException e) {
+                       log.error("Cannot initialize workspace " + workspaceName, e);
+               } finally {
+                       JcrUtils.logoutQuietly(workspaceAdminSession);
+               }
+       }
+
+       public void setJcrRepository(Repository jcrRepository) {
+               this.jcrRepository = jcrRepository;
+       }
+
+       public void setNodeIndexers(List<NodeIndexer> nodeIndexers) {
+               this.nodeIndexers = nodeIndexers;
+       }
+
+       public void setSecurityWorkspace(String securityWorkspace) {
+               this.securityWorkspace = securityWorkspace;
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/core/JavaRepoManagerImpl.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/core/JavaRepoManagerImpl.java
new file mode 100644 (file)
index 0000000..dfd0c8a
--- /dev/null
@@ -0,0 +1,8 @@
+package org.argeo.slc.repo.core;
+
+import org.argeo.slc.repo.JavaRepoManager;
+
+/** Java-specific operations */
+public class JavaRepoManagerImpl extends AbstractJcrRepoManager implements
+               JavaRepoManager {
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/core/RepoServiceImpl.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/core/RepoServiceImpl.java
new file mode 100644 (file)
index 0000000..6e2b7aa
--- /dev/null
@@ -0,0 +1,43 @@
+package org.argeo.slc.repo.core;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+
+import org.argeo.api.cms.keyring.Keyring;
+import org.argeo.slc.repo.RepoService;
+import org.argeo.slc.repo.RepoUtils;
+
+/**
+ * Work in Progress - enhance this. First implementation of a service that
+ * centralizes session management in an argeo SLC context, repositories are
+ * either defined using an URI and a workspace name in a anonymous context or
+ * using connection information that are store in a corresponding node in the
+ * local repository home
+ */
+public class RepoServiceImpl implements RepoService {
+
+       /* DEPENDENCY INJECTION */
+       private Repository nodeRepository;
+       private RepositoryFactory repositoryFactory;
+       private Keyring keyring;
+
+       public Session getRemoteSession(String repoNodePath, String uri,
+                       String workspaceName) {
+               return RepoUtils.getRemoteSession(repositoryFactory, keyring,
+                               nodeRepository, repoNodePath, uri, workspaceName);
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setNodeRepository(Repository nodeRepository) {
+               this.nodeRepository = nodeRepository;
+       }
+
+       public void setRepositoryFactory(RepositoryFactory repositoryFactory) {
+               this.repositoryFactory = repositoryFactory;
+       }
+
+       public void setKeyring(Keyring keyring) {
+               this.keyring = keyring;
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/core/RpmRepoManagerImpl.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/core/RpmRepoManagerImpl.java
new file mode 100644 (file)
index 0000000..6e83523
--- /dev/null
@@ -0,0 +1,8 @@
+package org.argeo.slc.repo.core;
+
+import org.argeo.slc.repo.RpmRepoManager;
+
+/** RPM-specific operations */
+public class RpmRepoManagerImpl extends AbstractJcrRepoManager implements
+               RpmRepoManager {
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/core/SlcRepoManagerImpl.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/core/SlcRepoManagerImpl.java
new file mode 100644 (file)
index 0000000..bce6f03
--- /dev/null
@@ -0,0 +1,38 @@
+package org.argeo.slc.repo.core;
+
+import org.argeo.slc.repo.JavaRepoManager;
+import org.argeo.slc.repo.RpmRepoManager;
+import org.argeo.slc.repo.SlcRepoManager;
+
+/** Coordinator of the various repositories. */
+public class SlcRepoManagerImpl implements SlcRepoManager {
+       private JavaRepoManager javaRepoManager;
+       private RpmRepoManager rpmRepoManager;
+
+       public void init() {
+
+       }
+
+       public void destroy() {
+
+       }
+
+       @Override
+       public JavaRepoManager getJavaRepoManager() {
+               return javaRepoManager;
+       }
+
+       public void setJavaRepoManager(JavaRepoManager javaRepoManager) {
+               this.javaRepoManager = javaRepoManager;
+       }
+
+       @Override
+       public RpmRepoManager getRpmRepoManager() {
+               return rpmRepoManager;
+       }
+
+       public void setRpmRepoManager(RpmRepoManager rpmRepoManager) {
+               this.rpmRepoManager = rpmRepoManager;
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/core/WorkspaceIndexer.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/core/WorkspaceIndexer.java
new file mode 100644 (file)
index 0000000..958165d
--- /dev/null
@@ -0,0 +1,81 @@
+package org.argeo.slc.repo.core;
+
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.EventListener;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.repo.NodeIndexer;
+
+/** Maintains the metadata of a workspace, using listeners */
+public class WorkspaceIndexer {
+       private final static CmsLog log = CmsLog.getLog(WorkspaceIndexer.class);
+
+       private final Session adminSession;
+       private IndexingListener artifactListener;
+       /** order may be important */
+       private final List<NodeIndexer> nodeIndexers;
+
+       public WorkspaceIndexer(Session adminSession, List<NodeIndexer> nodeIndexers) {
+               this.adminSession = adminSession;
+               this.nodeIndexers = nodeIndexers;
+               try {
+                       artifactListener = new IndexingListener();
+                       adminSession
+                                       .getWorkspace()
+                                       .getObservationManager()
+                                       .addEventListener(artifactListener, Event.NODE_ADDED, "/",
+                                                       true, null, null, true);
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot initialize repository backend", e);
+               }
+       }
+
+       public void close() {
+               try {
+                       adminSession.getWorkspace().getObservationManager()
+                                       .removeEventListener(artifactListener);
+               } catch (RepositoryException e) {
+                       log.error("Cannot close workspace indexer "
+                                       + adminSession.getWorkspace().getName(), e);
+               }
+       }
+
+       class IndexingListener implements EventListener {
+
+               public void onEvent(EventIterator events) {
+                       while (events.hasNext()) {
+                               Event event = events.nextEvent();
+                               try {
+                                       String newNodePath = event.getPath();
+                                       Node newNode = null;
+                                       for (NodeIndexer nodeIndexer : nodeIndexers) {
+                                               try {
+                                                       if (nodeIndexer.support(newNodePath)) {
+                                                               if (newNode == null)
+                                                                       newNode = adminSession.getNode(newNodePath);
+                                                               nodeIndexer.index(newNode);
+                                                       }
+                                               } catch (RuntimeException e) {
+                                                       e.printStackTrace();
+                                                       throw e;
+                                               }
+                                       }
+                                       if (newNode != null)
+                                               adminSession.save();
+                               } catch (RepositoryException e) {
+                                       throw new SlcException("Cannot process event " + event, e);
+                               } finally {
+                                       JcrUtils.discardQuietly(adminSession);
+                               }
+                       }
+               }
+       }
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/internal/springutil/AntPathMatcher.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/internal/springutil/AntPathMatcher.java
new file mode 100644 (file)
index 0000000..20becbc
--- /dev/null
@@ -0,0 +1,424 @@
+/*\r
+ * Copyright 2002-2007 the original author or authors.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package org.argeo.slc.repo.internal.springutil;\r
+\r
+/**\r
+ * PathMatcher implementation for Ant-style path patterns. Examples are provided\r
+ * below.\r
+ *\r
+ * <p>\r
+ * Part of this mapping code has been kindly borrowed from\r
+ * <a href="http://ant.apache.org">Apache Ant</a>.\r
+ *\r
+ * <p>\r
+ * The mapping matches URLs using the following rules:<br>\r
+ * <ul>\r
+ * <li>? matches one character</li>\r
+ * <li>* matches zero or more characters</li>\r
+ * <li>** matches zero or more 'directories' in a path</li>\r
+ * </ul>\r
+ *\r
+ * <p>\r
+ * Some examples:<br>\r
+ * <ul>\r
+ * <li><code>com/t?st.jsp</code> - matches <code>com/test.jsp</code> but also\r
+ * <code>com/tast.jsp</code> or <code>com/txst.jsp</code></li>\r
+ * <li><code>com/*.jsp</code> - matches all <code>.jsp</code> files in the\r
+ * <code>com</code> directory</li>\r
+ * <li><code>com/&#42;&#42;/test.jsp</code> - matches all <code>test.jsp</code>\r
+ * files underneath the <code>com</code> path</li>\r
+ * <li><code>org/springframework/&#42;&#42;/*.jsp</code> - matches all\r
+ * <code>.jsp</code> files underneath the <code>org/springframework</code>\r
+ * path</li>\r
+ * <li><code>org/&#42;&#42;/servlet/bla.jsp</code> - matches\r
+ * <code>org/springframework/servlet/bla.jsp</code> but also\r
+ * <code>org/springframework/testing/servlet/bla.jsp</code> and\r
+ * <code>org/servlet/bla.jsp</code></li>\r
+ * </ul>\r
+ *\r
+ * @author Alef Arendsen\r
+ * @author Juergen Hoeller\r
+ * @author Rob Harrop\r
+ * @since 16.07.2003\r
+ */\r
+public class AntPathMatcher implements PathMatcher {\r
+\r
+       /** Default path separator: "/" */\r
+       public static final String DEFAULT_PATH_SEPARATOR = "/";\r
+\r
+       private String pathSeparator = DEFAULT_PATH_SEPARATOR;\r
+\r
+       /**\r
+        * Set the path separator to use for pattern parsing. Default is "/", as in Ant.\r
+        */\r
+       public void setPathSeparator(String pathSeparator) {\r
+               this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR);\r
+       }\r
+\r
+       public boolean isPattern(String path) {\r
+               return (path.indexOf('*') != -1 || path.indexOf('?') != -1);\r
+       }\r
+\r
+       public boolean match(String pattern, String path) {\r
+               return doMatch(pattern, path, true);\r
+       }\r
+\r
+       public boolean matchStart(String pattern, String path) {\r
+               return doMatch(pattern, path, false);\r
+       }\r
+\r
+       /**\r
+        * Actually match the given <code>path</code> against the given\r
+        * <code>pattern</code>.\r
+        * \r
+        * @param pattern   the pattern to match against\r
+        * @param path      the path String to test\r
+        * @param fullMatch whether a full pattern match is required (else a pattern\r
+        *                  match as far as the given base path goes is sufficient)\r
+        * @return <code>true</code> if the supplied <code>path</code> matched,\r
+        *         <code>false</code> if it didn't\r
+        */\r
+       protected boolean doMatch(String pattern, String path, boolean fullMatch) {\r
+               if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {\r
+                       return false;\r
+               }\r
+\r
+//             String[] pattDirs = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator);\r
+//             String[] pathDirs = StringUtils.tokenizeToStringArray(path, this.pathSeparator);\r
+               // mbaudier - 2020-03-13 : Use standard Java call:\r
+               String[] pattDirs = pattern.split(this.pathSeparator);\r
+               String[] pathDirs = path.split(this.pathSeparator);\r
+\r
+               int pattIdxStart = 0;\r
+               int pattIdxEnd = pattDirs.length - 1;\r
+               int pathIdxStart = 0;\r
+               int pathIdxEnd = pathDirs.length - 1;\r
+\r
+               // Match all elements up to the first **\r
+               while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {\r
+                       String patDir = pattDirs[pattIdxStart];\r
+                       if ("**".equals(patDir)) {\r
+                               break;\r
+                       }\r
+                       if (!matchStrings(patDir, pathDirs[pathIdxStart])) {\r
+                               return false;\r
+                       }\r
+                       pattIdxStart++;\r
+                       pathIdxStart++;\r
+               }\r
+\r
+               if (pathIdxStart > pathIdxEnd) {\r
+                       // Path is exhausted, only match if rest of pattern is * or **'s\r
+                       if (pattIdxStart > pattIdxEnd) {\r
+                               return (pattern.endsWith(this.pathSeparator) ? path.endsWith(this.pathSeparator)\r
+                                               : !path.endsWith(this.pathSeparator));\r
+                       }\r
+                       if (!fullMatch) {\r
+                               return true;\r
+                       }\r
+                       if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) {\r
+                               return true;\r
+                       }\r
+                       for (int i = pattIdxStart; i <= pattIdxEnd; i++) {\r
+                               if (!pattDirs[i].equals("**")) {\r
+                                       return false;\r
+                               }\r
+                       }\r
+                       return true;\r
+               } else if (pattIdxStart > pattIdxEnd) {\r
+                       // String not exhausted, but pattern is. Failure.\r
+                       return false;\r
+               } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {\r
+                       // Path start definitely matches due to "**" part in pattern.\r
+                       return true;\r
+               }\r
+\r
+               // up to last '**'\r
+               while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {\r
+                       String patDir = pattDirs[pattIdxEnd];\r
+                       if (patDir.equals("**")) {\r
+                               break;\r
+                       }\r
+                       if (!matchStrings(patDir, pathDirs[pathIdxEnd])) {\r
+                               return false;\r
+                       }\r
+                       pattIdxEnd--;\r
+                       pathIdxEnd--;\r
+               }\r
+               if (pathIdxStart > pathIdxEnd) {\r
+                       // String is exhausted\r
+                       for (int i = pattIdxStart; i <= pattIdxEnd; i++) {\r
+                               if (!pattDirs[i].equals("**")) {\r
+                                       return false;\r
+                               }\r
+                       }\r
+                       return true;\r
+               }\r
+\r
+               while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {\r
+                       int patIdxTmp = -1;\r
+                       for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {\r
+                               if (pattDirs[i].equals("**")) {\r
+                                       patIdxTmp = i;\r
+                                       break;\r
+                               }\r
+                       }\r
+                       if (patIdxTmp == pattIdxStart + 1) {\r
+                               // '**/**' situation, so skip one\r
+                               pattIdxStart++;\r
+                               continue;\r
+                       }\r
+                       // Find the pattern between padIdxStart & padIdxTmp in str between\r
+                       // strIdxStart & strIdxEnd\r
+                       int patLength = (patIdxTmp - pattIdxStart - 1);\r
+                       int strLength = (pathIdxEnd - pathIdxStart + 1);\r
+                       int foundIdx = -1;\r
+\r
+                       strLoop: for (int i = 0; i <= strLength - patLength; i++) {\r
+                               for (int j = 0; j < patLength; j++) {\r
+                                       String subPat = (String) pattDirs[pattIdxStart + j + 1];\r
+                                       String subStr = (String) pathDirs[pathIdxStart + i + j];\r
+                                       if (!matchStrings(subPat, subStr)) {\r
+                                               continue strLoop;\r
+                                       }\r
+                               }\r
+                               foundIdx = pathIdxStart + i;\r
+                               break;\r
+                       }\r
+\r
+                       if (foundIdx == -1) {\r
+                               return false;\r
+                       }\r
+\r
+                       pattIdxStart = patIdxTmp;\r
+                       pathIdxStart = foundIdx + patLength;\r
+               }\r
+\r
+               for (int i = pattIdxStart; i <= pattIdxEnd; i++) {\r
+                       if (!pattDirs[i].equals("**")) {\r
+                               return false;\r
+                       }\r
+               }\r
+\r
+               return true;\r
+       }\r
+\r
+       /**\r
+        * Tests whether or not a string matches against a pattern. The pattern may\r
+        * contain two special characters:<br>\r
+        * '*' means zero or more characters<br>\r
+        * '?' means one and only one character\r
+        * \r
+        * @param pattern pattern to match against. Must not be <code>null</code>.\r
+        * @param str     string which must be matched against the pattern. Must not be\r
+        *                <code>null</code>.\r
+        * @return <code>true</code> if the string matches against the pattern, or\r
+        *         <code>false</code> otherwise.\r
+        */\r
+       private boolean matchStrings(String pattern, String str) {\r
+               char[] patArr = pattern.toCharArray();\r
+               char[] strArr = str.toCharArray();\r
+               int patIdxStart = 0;\r
+               int patIdxEnd = patArr.length - 1;\r
+               int strIdxStart = 0;\r
+               int strIdxEnd = strArr.length - 1;\r
+               char ch;\r
+\r
+               boolean containsStar = false;\r
+               for (int i = 0; i < patArr.length; i++) {\r
+                       if (patArr[i] == '*') {\r
+                               containsStar = true;\r
+                               break;\r
+                       }\r
+               }\r
+\r
+               if (!containsStar) {\r
+                       // No '*'s, so we make a shortcut\r
+                       if (patIdxEnd != strIdxEnd) {\r
+                               return false; // Pattern and string do not have the same size\r
+                       }\r
+                       for (int i = 0; i <= patIdxEnd; i++) {\r
+                               ch = patArr[i];\r
+                               if (ch != '?') {\r
+                                       if (ch != strArr[i]) {\r
+                                               return false;// Character mismatch\r
+                                       }\r
+                               }\r
+                       }\r
+                       return true; // String matches against pattern\r
+               }\r
+\r
+               if (patIdxEnd == 0) {\r
+                       return true; // Pattern contains only '*', which matches anything\r
+               }\r
+\r
+               // Process characters before first star\r
+               while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) {\r
+                       if (ch != '?') {\r
+                               if (ch != strArr[strIdxStart]) {\r
+                                       return false;// Character mismatch\r
+                               }\r
+                       }\r
+                       patIdxStart++;\r
+                       strIdxStart++;\r
+               }\r
+               if (strIdxStart > strIdxEnd) {\r
+                       // All characters in the string are used. Check if only '*'s are\r
+                       // left in the pattern. If so, we succeeded. Otherwise failure.\r
+                       for (int i = patIdxStart; i <= patIdxEnd; i++) {\r
+                               if (patArr[i] != '*') {\r
+                                       return false;\r
+                               }\r
+                       }\r
+                       return true;\r
+               }\r
+\r
+               // Process characters after last star\r
+               while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) {\r
+                       if (ch != '?') {\r
+                               if (ch != strArr[strIdxEnd]) {\r
+                                       return false;// Character mismatch\r
+                               }\r
+                       }\r
+                       patIdxEnd--;\r
+                       strIdxEnd--;\r
+               }\r
+               if (strIdxStart > strIdxEnd) {\r
+                       // All characters in the string are used. Check if only '*'s are\r
+                       // left in the pattern. If so, we succeeded. Otherwise failure.\r
+                       for (int i = patIdxStart; i <= patIdxEnd; i++) {\r
+                               if (patArr[i] != '*') {\r
+                                       return false;\r
+                               }\r
+                       }\r
+                       return true;\r
+               }\r
+\r
+               // process pattern between stars. padIdxStart and patIdxEnd point\r
+               // always to a '*'.\r
+               while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {\r
+                       int patIdxTmp = -1;\r
+                       for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {\r
+                               if (patArr[i] == '*') {\r
+                                       patIdxTmp = i;\r
+                                       break;\r
+                               }\r
+                       }\r
+                       if (patIdxTmp == patIdxStart + 1) {\r
+                               // Two stars next to each other, skip the first one.\r
+                               patIdxStart++;\r
+                               continue;\r
+                       }\r
+                       // Find the pattern between padIdxStart & padIdxTmp in str between\r
+                       // strIdxStart & strIdxEnd\r
+                       int patLength = (patIdxTmp - patIdxStart - 1);\r
+                       int strLength = (strIdxEnd - strIdxStart + 1);\r
+                       int foundIdx = -1;\r
+                       strLoop: for (int i = 0; i <= strLength - patLength; i++) {\r
+                               for (int j = 0; j < patLength; j++) {\r
+                                       ch = patArr[patIdxStart + j + 1];\r
+                                       if (ch != '?') {\r
+                                               if (ch != strArr[strIdxStart + i + j]) {\r
+                                                       continue strLoop;\r
+                                               }\r
+                                       }\r
+                               }\r
+\r
+                               foundIdx = strIdxStart + i;\r
+                               break;\r
+                       }\r
+\r
+                       if (foundIdx == -1) {\r
+                               return false;\r
+                       }\r
+\r
+                       patIdxStart = patIdxTmp;\r
+                       strIdxStart = foundIdx + patLength;\r
+               }\r
+\r
+               // All characters in the string are used. Check if only '*'s are left\r
+               // in the pattern. If so, we succeeded. Otherwise failure.\r
+               for (int i = patIdxStart; i <= patIdxEnd; i++) {\r
+                       if (patArr[i] != '*') {\r
+                               return false;\r
+                       }\r
+               }\r
+\r
+               return true;\r
+       }\r
+\r
+       /**\r
+        * Given a pattern and a full path, determine the pattern-mapped part.\r
+        * <p>\r
+        * For example:\r
+        * <ul>\r
+        * <li>'<code>/docs/cvs/commit.html</code>' and\r
+        * '<code>/docs/cvs/commit.html</code> to ''</li>\r
+        * <li>'<code>/docs/*</code>' and '<code>/docs/cvs/commit</code> to\r
+        * '<code>cvs/commit</code>'</li>\r
+        * <li>'<code>/docs/cvs/*.html</code>' and '<code>/docs/cvs/commit.html</code>\r
+        * to '<code>commit.html</code>'</li>\r
+        * <li>'<code>/docs/**</code>' and '<code>/docs/cvs/commit</code> to\r
+        * '<code>cvs/commit</code>'</li>\r
+        * <li>'<code>/docs/**\/*.html</code>' and '<code>/docs/cvs/commit.html</code>\r
+        * to '<code>cvs/commit.html</code>'</li>\r
+        * <li>'<code>/*.html</code>' and '<code>/docs/cvs/commit.html</code> to\r
+        * '<code>docs/cvs/commit.html</code>'</li>\r
+        * <li>'<code>*.html</code>' and '<code>/docs/cvs/commit.html</code> to\r
+        * '<code>/docs/cvs/commit.html</code>'</li>\r
+        * <li>'<code>*</code>' and '<code>/docs/cvs/commit.html</code> to\r
+        * '<code>/docs/cvs/commit.html</code>'</li>\r
+        * </ul>\r
+        * <p>\r
+        * Assumes that {@link #match} returns <code>true</code> for\r
+        * '<code>pattern</code>' and '<code>path</code>', but does <strong>not</strong>\r
+        * enforce this.\r
+        */\r
+       public String extractPathWithinPattern(String pattern, String path) {\r
+//             String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator);\r
+//             String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator);\r
+               // mbaudier - 2020-03-13 : Use standard Java call:\r
+               String[] patternParts = pattern.split(this.pathSeparator);\r
+               String[] pathParts = path.split(this.pathSeparator);\r
+\r
+               StringBuffer buffer = new StringBuffer();\r
+\r
+               // Add any path parts that have a wildcarded pattern part.\r
+               int puts = 0;\r
+               for (int i = 0; i < patternParts.length; i++) {\r
+                       String patternPart = patternParts[i];\r
+                       if ((patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) && pathParts.length >= i + 1) {\r
+                               if (puts > 0 || (i == 0 && !pattern.startsWith(this.pathSeparator))) {\r
+                                       buffer.append(this.pathSeparator);\r
+                               }\r
+                               buffer.append(pathParts[i]);\r
+                               puts++;\r
+                       }\r
+               }\r
+\r
+               // Append any trailing path parts.\r
+               for (int i = patternParts.length; i < pathParts.length; i++) {\r
+                       if (puts > 0 || i > 0) {\r
+                               buffer.append(this.pathSeparator);\r
+                       }\r
+                       buffer.append(pathParts[i]);\r
+               }\r
+\r
+               return buffer.toString();\r
+       }\r
+\r
+}\r
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/internal/springutil/PathMatcher.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/internal/springutil/PathMatcher.java
new file mode 100644 (file)
index 0000000..9fc1f22
--- /dev/null
@@ -0,0 +1,91 @@
+/*\r
+ * Copyright 2002-2007 the original author or authors.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package org.argeo.slc.repo.internal.springutil;\r
+\r
+/**\r
+ * Strategy interface for <code>String</code>-based path matching.\r
+ * \r
+ * <p>Used by <code>org.springframework.core.io.support.PathMatchingResourcePatternResolver</code>,\r
+ * {@link org.springframework.web.servlet.handler.AbstractUrlHandlerMapping},\r
+ * {@link org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver},\r
+ * and {@link org.springframework.web.servlet.mvc.WebContentInterceptor}.\r
+ *\r
+ * <p>The default implementation is {@link AntPathMatcher}, supporting the\r
+ * Ant-style pattern syntax.\r
+ *\r
+ * @author Juergen Hoeller\r
+ * @since 1.2\r
+ * @see AntPathMatcher\r
+ */\r
+public interface PathMatcher {\r
+\r
+       /**\r
+        * Does the given <code>path</code> represent a pattern that can be matched\r
+        * by an implementation of this interface?\r
+        * <p>If the return value is <code>false</code>, then the {@link #match}\r
+        * method does not have to be used because direct equality comparisons\r
+        * on the static path Strings will lead to the same result.\r
+        * @param path the path String to check\r
+        * @return <code>true</code> if the given <code>path</code> represents a pattern\r
+        */\r
+       boolean isPattern(String path);\r
+\r
+       /**\r
+        * Match the given <code>path</code> against the given <code>pattern</code>,\r
+        * according to this PathMatcher's matching strategy.\r
+        * @param pattern the pattern to match against\r
+        * @param path the path String to test\r
+        * @return <code>true</code> if the supplied <code>path</code> matched,\r
+        * <code>false</code> if it didn't\r
+        */\r
+       boolean match(String pattern, String path);\r
+\r
+       /**\r
+        * Match the given <code>path</code> against the corresponding part of the given\r
+        * <code>pattern</code>, according to this PathMatcher's matching strategy.\r
+        * <p>Determines whether the pattern at least matches as far as the given base\r
+        * path goes, assuming that a full path may then match as well.\r
+        * @param pattern the pattern to match against\r
+        * @param path the path String to test\r
+        * @return <code>true</code> if the supplied <code>path</code> matched,\r
+        * <code>false</code> if it didn't\r
+        */\r
+       boolean matchStart(String pattern, String path);\r
+\r
+       /**\r
+        * Given a pattern and a full path, determine the pattern-mapped part.\r
+        * <p>This method is supposed to find out which part of the path is matched\r
+        * dynamically through an actual pattern, that is, it strips off a statically\r
+        * defined leading path from the given full path, returning only the actually\r
+        * pattern-matched part of the path.\r
+        * <p>For example: For "myroot/*.html" as pattern and "myroot/myfile.html"\r
+        * as full path, this method should return "myfile.html". The detailed\r
+        * determination rules are specified to this PathMatcher's matching strategy.\r
+        * <p>A simple implementation may return the given full path as-is in case\r
+        * of an actual pattern, and the empty String in case of the pattern not\r
+        * containing any dynamic parts (i.e. the <code>pattern</code> parameter being\r
+        * a static path that wouldn't qualify as an actual {@link #isPattern pattern}).\r
+        * A sophisticated implementation will differentiate between the static parts\r
+        * and the dynamic parts of the given path pattern.\r
+        * @param pattern the path pattern\r
+        * @param path the full path to introspect\r
+        * @return the pattern-mapped part of the given <code>path</code>\r
+        * (never <code>null</code>)\r
+        */\r
+       String extractPathWithinPattern(String pattern, String path);\r
+\r
+}\r
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/license/apache-2.0.txt b/org.argeo.slc.repo/src/org/argeo/slc/repo/license/apache-2.0.txt
new file mode 100644 (file)
index 0000000..d645695
--- /dev/null
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/license/bsd-3-clause.txt b/org.argeo.slc.repo/src/org/argeo/slc/repo/license/bsd-3-clause.txt
new file mode 100644 (file)
index 0000000..ed0116e
--- /dev/null
@@ -0,0 +1,12 @@
+Copyright (c) <YEAR>, <OWNER>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/license/cddl-1.0.txt b/org.argeo.slc.repo/src/org/argeo/slc/repo/license/cddl-1.0.txt
new file mode 100644 (file)
index 0000000..9dc4442
--- /dev/null
@@ -0,0 +1,93 @@
+ COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0 1.
+
+Definitions.
+
+1.1. Contributor means each individual or entity that creates or contributes to the creation of Modifications.
+
+1.2. Contributor Version means the combination of the Original Software, prior Modifications used by a Contributor (if any), and the Modifications made by that particular Contributor.
+
+1.3. Covered Software means (a) the Original Software, or (b) Modifications, or (c) the combination of files containing Original Software with files containing Modifications, in each case including portions thereof.
+
+1.4. Executable means the Covered Software in any form other than Source Code.
+
+1.5. Initial Developer means the individual or entity that first makes Original Software available under this License.
+
+1.6. Larger Work means a work which combines Covered Software or portions thereof with code not governed by the terms of this License.
+
+1.7. License means this document.
+
+1.8. Licensable means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein.
+
+1.9. Modifications means the Source Code and Executable form of any of the following: A. Any file that results from an addition to, deletion from or modification of the contents of a file containing Original Software or previous Modifications; B. Any new file that contains any part of the Original Software or previous Modification; or C. Any new file that is contributed or otherwise made available under the terms of this License.
+
+1.10. Original Software means the Source Code and Executable form of computer software code that is originally released under this License.
+
+1.11. Patent Claims means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor.
+
+1.12. Source Code means (a) the common form of computer software code in which modifications are made and (b) associated documentation included in or with such code.
+
+1.13. You (or Your) means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, You includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, control means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity.
+
+2. License Grants.
+
+ 2.1. The Initial Developer Grant. Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, the Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer, to use, reproduce, modify, display, perform, sublicense and distribute the Original Software (or portions thereof), with or without Modifications, and/or as part of a Larger Work; and
+
+(b) under Patent Claims infringed by the making, using or selling of Original Software, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Software (or portions thereof);
+
+ (c) The licenses granted in Sections 2.1(a) and (b) are effective on the date Initial Developer first distributes or otherwise makes the Original Software available to a third party under the terms of this License;
+
+ (d) Notwithstanding Section 2.1(b) above, no patent license is granted: (1) for code that You delete from the Original Software, or (2) for infringements caused by: (i) the modification of the Original Software, or (ii) the combination of the Original Software with other software or devices.
+
+2.2. Contributor Grant. Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark) Licensable by Contributor to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof), either on an unmodified basis, with other Modifications, as Covered Software and/or as part of a Larger Work; and
+
+(b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: (1) Modifications made by that Contributor (or portions thereof); and (2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination).
+
+(c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first distributes or otherwise makes the Modifications available to a third party.
+
+(d) Notwithstanding Section 2.2(b) above, no patent license is granted: (1) for any code that Contributor has deleted from the Contributor Version; (2) for infringements caused by: (i) third party modifications of Contributor Version, or (ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or (3) under Patent Claims infringed by Covered Software in the absence of Modifications made by that Contributor.
+
+3. Distribution Obligations.
+
+3.1. Availability of Source Code. Any Covered Software that You distribute or otherwise make available in Executable form must also be made available in Source Code form and that Source Code form must be distributed only under the terms of this License. You must include a copy of this License with every copy of the Source Code form of the Covered Software You distribute or otherwise make available. You must inform recipients of any such Covered Software in Executable form as to how they can obtain such Covered Software in Source Code form in a reasonable manner on or through a medium customarily used for software exchange.
+
+3.2. Modifications. The Modifications that You create or to which You contribute are governed by the terms of this License. You represent that You believe Your Modifications are Your original creation(s) and/or You have sufficient rights to grant the rights conveyed by this License.
+
+3.3. Required Notices. You must include a notice in each of Your Modifications that identifies You as the Contributor of the Modification. You may not remove or alter any copyright, patent or trademark notices contained within the Covered Software, or any notices of licensing or any descriptive text giving attribution to any Contributor or the Initial Developer.
+
+3.4. Application of Additional Terms. You may not offer or impose any terms on any Covered Software in Source Code form that alters or restricts the applicable version of this License or the recipients rights hereunder. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, you may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear that any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer.
+
+3.5. Distribution of Executable Versions. You may distribute the Executable form of the Covered Software under the terms of this License or under the terms of a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable form does not attempt to limit or alter the recipients rights in the Source Code form from the rights set forth in this License. If You distribute the Covered Software in Executable form under a different license, You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer.
+
+3.6. Larger Works. You may create a Larger Work by combining Covered Software with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Software.
+
+4. Versions of the License.
+
+4.1. New Versions. Sun Microsystems, Inc. is the initial license steward and may publish revised and/or new versions of this License from time to time. Each version will be given a distinguishing version number. Except as provided in Section 4.3, no one other than the license steward has the right to modify this License.
+
+4.2. Effect of New Versions. You may always continue to use, distribute or otherwise make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. If the Initial Developer includes a notice in the Original Software prohibiting it from being distributed or otherwise made available under any subsequent version of the License, You must distribute and make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. Otherwise, You may also choose to use, distribute or otherwise make the Covered Software available under the terms of any subsequent version of the License published by the license steward.
+
+4.3. Modified Versions. When You are an Initial Developer and You want to create a new license for Your Original Software, You may create and use a modified version of this License if You: (a) rename the license and remove any references to the name of the license steward (except to note that the license differs from this License); and (b) otherwise make it clear that the license contains terms which differ from this License.
+
+5. DISCLAIMER OF WARRANTY. COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN AS IS BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
+
+6. TERMINATION.
+
+6.1. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive.
+
+6.2. If You assert a patent infringement claim (excluding declaratory judgment actions) against Initial Developer or a Contributor (the Initial Developer or Contributor against whom You assert such claim is referred to as Participant) alleging that the Participant Software (meaning the Contributor Version where the Participant is a Contributor or the Original Software where the Participant is the Initial Developer) directly or indirectly infringes any patent, then any and all rights granted directly or indirectly to You by such Participant, the Initial Developer (if the Initial Developer is not the Participant) and all Contributors under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively and automatically at the expiration of such 60 day notice period, unless if within such 60 day period You withdraw Your claim with respect to the Participant Software against such Participant either unilaterally or pursuant to a written agreement with Participant.
+
+6.3. In the event of termination under Sections 6.1 or 6.2 above, all end user licenses that have been validly granted by You or any distributor hereunder prior to termination (excluding licenses granted to You by any distributor) shall survive termination.
+
+7. LIMITATION OF LIABILITY. UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTYS NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
+
+8. U.S. GOVERNMENT END USERS. The Covered Software is a commercial item, as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of commercial computer software (as that term is defined at 48 C.F.R.  252.227-7014(a)(1)) and commercial computer software documentation as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Software with only those rights set forth herein. This U.S. Government Rights clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or provision that addresses Government rights in computer software under this License.
+
+9. MISCELLANEOUS. This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by the law of the jurisdiction specified in a notice contained within the Original Software (except to the extent applicable law, if any, provides otherwise), excluding such jurisdictions conflict-of-law provisions. Any litigation relating to this License shall be subject to the jurisdiction of the courts located in the jurisdiction and venue specified in a notice contained within the Original Software, with the losing party responsible for costs, including, without limitation, court costs and reasonable attorneys fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. You agree that You alone are responsible for compliance with the United States export administration regulations (and the export control laws and regulation of any other countries) when You use, distribute or otherwise make available any Covered Software.
+
+10. RESPONSIBILITY FOR CLAIMS. As between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability.
+
+NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) The code released under the CDDL shall be governed by the laws of the State of California (excluding conflict-of-law provisions). Any litigation relating to this License shall be subject to the jurisdiction of the Federal Courts of the Northern District of California and the state courts of the State of California, with venue lying in Santa Clara County, California. 
\ No newline at end of file
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/license/epl-1.0.txt b/org.argeo.slc.repo/src/org/argeo/slc/repo/license/epl-1.0.txt
new file mode 100644 (file)
index 0000000..795c0c3
--- /dev/null
@@ -0,0 +1,73 @@
+ THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+    a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and
+    b) in the case of each subsequent Contributor:
+    i) changes to the Program, and
+    ii) additions to the Program; 
+
+where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents " mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program.
+
+"Program" means the Contributions distributed in accordance with this Agreement.
+
+"Recipient" means anyone who receives the Program under this Agreement, including all Contributors.
+
+2. GRANT OF RIGHTS
+
+    a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form.
+    b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder.
+    c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program.
+    d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. 
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that:
+
+    a) it complies with the terms and conditions of this Agreement; and
+    b) its license agreement:
+    i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose;
+    ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits;
+    iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and
+    iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. 
+
+When the Program is made available in source code form:
+
+    a) it must be made available under this Agreement; and
+    b) a copy of this Agreement must be included with each copy of the Program. 
+
+Contributors may not remove or alter any copyright notices contained within the Program.
+
+Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.
+
+If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive.
+
+Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation.
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/license/gpl-2.0.txt b/org.argeo.slc.repo/src/org/argeo/slc/repo/license/gpl-2.0.txt
new file mode 100644 (file)
index 0000000..d159169
--- /dev/null
@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/license/gpl-3.0.txt b/org.argeo.slc.repo/src/org/argeo/slc/repo/license/gpl-3.0.txt
new file mode 100644 (file)
index 0000000..94a9ed0
--- /dev/null
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/license/lgpl-2.1.txt b/org.argeo.slc.repo/src/org/argeo/slc/repo/license/lgpl-2.1.txt
new file mode 100644 (file)
index 0000000..4362b49
--- /dev/null
@@ -0,0 +1,502 @@
+                  GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+\f
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+\f
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+\f
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+\f
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+\f
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+\f
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+\f
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+\f
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+\f
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/license/lgpl-3.0.txt b/org.argeo.slc.repo/src/org/argeo/slc/repo/license/lgpl-3.0.txt
new file mode 100644 (file)
index 0000000..65c5ca8
--- /dev/null
@@ -0,0 +1,165 @@
+                   GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+  This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+  0. Additional Definitions.
+
+  As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+  "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+  An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+  A "Combined Work" is a work produced by combining or linking an
+Application with the Library.  The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+  The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+  The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+  1. Exception to Section 3 of the GNU GPL.
+
+  You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+  2. Conveying Modified Versions.
+
+  If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+   a) under this License, provided that you make a good faith effort to
+   ensure that, in the event an Application does not supply the
+   function or data, the facility still operates, and performs
+   whatever part of its purpose remains meaningful, or
+
+   b) under the GNU GPL, with none of the additional permissions of
+   this License applicable to that copy.
+
+  3. Object Code Incorporating Material from Library Header Files.
+
+  The object code form of an Application may incorporate material from
+a header file that is part of the Library.  You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+   a) Give prominent notice with each copy of the object code that the
+   Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the object code with a copy of the GNU GPL and this license
+   document.
+
+  4. Combined Works.
+
+  You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+   a) Give prominent notice with each copy of the Combined Work that
+   the Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the Combined Work with a copy of the GNU GPL and this license
+   document.
+
+   c) For a Combined Work that displays copyright notices during
+   execution, include the copyright notice for the Library among
+   these notices, as well as a reference directing the user to the
+   copies of the GNU GPL and this license document.
+
+   d) Do one of the following:
+
+       0) Convey the Minimal Corresponding Source under the terms of this
+       License, and the Corresponding Application Code in a form
+       suitable for, and under terms that permit, the user to
+       recombine or relink the Application with a modified version of
+       the Linked Version to produce a modified Combined Work, in the
+       manner specified by section 6 of the GNU GPL for conveying
+       Corresponding Source.
+
+       1) Use a suitable shared library mechanism for linking with the
+       Library.  A suitable mechanism is one that (a) uses at run time
+       a copy of the Library already present on the user's computer
+       system, and (b) will operate properly with a modified version
+       of the Library that is interface-compatible with the Linked
+       Version.
+
+   e) Provide Installation Information, but only if you would otherwise
+   be required to provide such information under section 6 of the
+   GNU GPL, and only to the extent that such information is
+   necessary to install and execute a modified version of the
+   Combined Work produced by recombining or relinking the
+   Application with a modified version of the Linked Version. (If
+   you use option 4d0, the Installation Information must accompany
+   the Minimal Corresponding Source and Corresponding Application
+   Code. If you use option 4d1, you must provide the Installation
+   Information in the manner specified by section 6 of the GNU GPL
+   for conveying Corresponding Source.)
+
+  5. Combined Libraries.
+
+  You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+   a) Accompany the combined library with a copy of the same work based
+   on the Library, uncombined with any other library facilities,
+   conveyed under the terms of this License.
+
+   b) Give prominent notice with the combined library that part of it
+   is a work based on the Library, and explaining where to find the
+   accompanying uncombined form of the same work.
+
+  6. Revised Versions of the GNU Lesser General Public License.
+
+  The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+  Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+  If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/license/mit.txt b/org.argeo.slc.repo/src/org/argeo/slc/repo/license/mit.txt
new file mode 100644 (file)
index 0000000..e14c371
--- /dev/null
@@ -0,0 +1,17 @@
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/maven/AetherUtils.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/maven/AetherUtils.java
new file mode 100644 (file)
index 0000000..e8b07c1
--- /dev/null
@@ -0,0 +1,161 @@
+package org.argeo.slc.repo.maven;
+
+import java.util.regex.Pattern;
+
+import org.apache.commons.io.FilenameUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.slc.SlcException;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.graph.DependencyNode;
+
+/** Utilities related to Aether */
+public class AetherUtils {
+       public final static String SNAPSHOT = "SNAPSHOT";
+       // hacked from aether
+       public static final Pattern SNAPSHOT_TIMESTAMP = Pattern
+                       .compile("^(.*-)?([0-9]{8}.[0-9]{6}-[0-9]+)$");
+
+       private final static CmsLog log = CmsLog.getLog(AetherUtils.class);
+
+       /** Logs a dependency node and its transitive dependencies as a tree. */
+       public static void logDependencyNode(int depth,
+                       DependencyNode dependencyNode) {
+               if (!log.isDebugEnabled())
+                       return;
+
+               StringBuffer prefix = new StringBuffer(depth * 2 + 2);
+               // prefix.append("|-");
+               for (int i = 0; i < depth * 2; i++) {
+                       prefix.append(' ');
+               }
+               Artifact artifact = dependencyNode.getDependency().getArtifact();
+               log.debug(prefix + "|-> " + artifact.getArtifactId() + " ["
+                               + artifact.getVersion() + "]"
+                               + (dependencyNode.getDependency().isOptional() ? " ?" : ""));
+               for (DependencyNode child : dependencyNode.getChildren()) {
+                       logDependencyNode(depth + 1, child);
+               }
+       }
+
+       /**
+        * Converts a path (relative to a repository root) to an {@link Artifact}.
+        * 
+        * @param path
+        *            the relative path
+        * @param type
+        *            the layout type, currently ignored because only the 'default'
+        *            Maven 2 layout is currently supported:
+        *            /my/group/id/artifactId/
+        *            version/artifactId-version[-classifier].extension
+        * @return the related artifact or null if the file is not an artifact
+        *         (Maven medata data XML files, check sums, etc.)
+        */
+       public static Artifact convertPathToArtifact(String path, String type) {
+               // TODO rewrite it with regexp (unit tests first!)
+
+               // normalize
+               if (path.startsWith("/"))
+                       path = path.substring(1);
+
+               // parse group id
+               String[] tokensSlash = path.split("/");
+               if (tokensSlash.length < 4)
+                       return null;
+               StringBuffer groupId = new StringBuffer(path.length());
+               for (int i = 0; i < tokensSlash.length - 3; i++) {
+                       if (i != 0)
+                               groupId.append('.');
+                       groupId.append(tokensSlash[i]);
+               }
+               String artifactId = tokensSlash[tokensSlash.length - 3];
+               String baseVersion = tokensSlash[tokensSlash.length - 2];
+               String fileName = tokensSlash[tokensSlash.length - 1];
+
+               if (!fileName.startsWith(artifactId))
+                       return null;
+               // FIXME make it configurable? (via an argument?)
+               if (FilenameUtils.isExtension(fileName, new String[] { "sha1", "md5" }))
+                       return null;
+
+               String extension = FilenameUtils.getExtension(fileName);
+               String baseName = FilenameUtils.getBaseName(fileName);
+
+               // check since we assume hereafter
+               if (!baseName.startsWith(artifactId))
+                       throw new SlcException("Base name '" + baseName
+                                       + " does not start with artifact id '" + artifactId
+                                       + "' in " + path);
+
+               boolean isSnapshot = baseVersion.endsWith("-" + SNAPSHOT);
+               String baseBaseVersion = isSnapshot ? baseVersion.substring(0,
+                               baseVersion.length() - SNAPSHOT.length() - 1) : baseVersion;
+               int artifactAndBaseBaseVersionLength = artifactId.length() + 1
+                               + baseBaseVersion.length() + 1;
+               String classifier = null;
+               if (baseName.length() > artifactAndBaseBaseVersionLength) {
+                       String dashRest = baseName
+                                       .substring(artifactAndBaseBaseVersionLength);
+                       String[] dashes = dashRest.split("-");
+
+                       if (isSnapshot) {
+                               if (dashes[0].equals(SNAPSHOT)) {
+                                       if (dashRest.length() > SNAPSHOT.length() + 1)
+                                               classifier = dashRest.substring(SNAPSHOT.length() + 1);
+
+                               } else {
+                                       if (dashes.length > 2)// assume no '-' in classifier
+                                               classifier = dashes[2];
+                               }
+                       } else {
+                               if (dashes.length > 0)
+                                       classifier = dashes[0];
+                       }
+               }
+
+               // classifier
+               // String classifier = null;
+               // int firstDash = baseName.indexOf('-');
+               // int classifierDash = baseName.lastIndexOf('-');
+               // if (classifierDash > 0 && classifierDash != firstDash) {
+               // classifier = baseName.substring(classifierDash + 1);
+               // }
+               // if (isSnapshot && classifier != null) {
+               // if (classifier.equals(SNAPSHOT))
+               // classifier = null;
+               // else
+               // try {
+               // Long.parseLong(classifier); // build number
+               // // if not failed this is a timestamped version
+               // classifier = null;
+               // } catch (NumberFormatException e) {
+               // // silent
+               // }
+               // }
+
+               // version
+               String version = baseName.substring(artifactId.length() + 1);
+               if (classifier != null)
+                       version = version.substring(0,
+                                       version.length() - classifier.length() - 1);
+
+               // consistency checks
+               if (!isSnapshot && !version.equals(baseVersion))
+                       throw new SlcException("Base version '" + baseVersion
+                                       + "' and version '" + version + "' not in line in " + path);
+               if (!isSnapshot && isSnapshotVersion(version))
+                       throw new SlcException("SNAPSHOT base version '" + baseVersion
+                                       + "' and version '" + version + "' not in line in " + path);
+
+               DefaultArtifact artifact = new DefaultArtifact(groupId.toString(),
+                               artifactId, classifier, extension, version);
+               return artifact;
+       }
+
+       /** Hacked from aether */
+       public static boolean isSnapshotVersion(String version) {
+               return version.endsWith(SNAPSHOT)
+                               || SNAPSHOT_TIMESTAMP.matcher(version).matches();
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/maven/ArtifactIdComparator.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/maven/ArtifactIdComparator.java
new file mode 100644 (file)
index 0000000..7aef78d
--- /dev/null
@@ -0,0 +1,20 @@
+package org.argeo.slc.repo.maven;
+
+import java.util.Comparator;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * Compare two artifacts, for use in {@link TreeSet} / {@link TreeMap}, consider
+ * artifactId first THEN groupId
+ */
+public class ArtifactIdComparator implements Comparator<Artifact> {
+       public int compare(Artifact o1, Artifact o2) {
+               if (o1.getArtifactId().equals(o2.getArtifactId()))
+                       return o1.getGroupId().compareTo(o2.getGroupId());
+               return o1.getArtifactId().compareTo(o2.getArtifactId());
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/maven/ConvertPoms_01_03.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/maven/ConvertPoms_01_03.java
new file mode 100644 (file)
index 0000000..d46375e
--- /dev/null
@@ -0,0 +1,211 @@
+package org.argeo.slc.repo.maven;
+
+import java.io.File;
+import java.util.HashMap;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/** Recursively migrate all the POMs to Argeo Distribution v1.3 */
+public class ConvertPoms_01_03 implements Runnable {
+       final String SPRING_SOURCE_PREFIX = "com.springsource";
+
+       private HashMap<String, String> artifactMapping = new HashMap<String, String>();
+
+       private File rootDir;
+
+       public ConvertPoms_01_03(String rootDirPath) {
+               this(new File(rootDirPath));
+       }
+
+       public ConvertPoms_01_03(File rootDir) {
+               this.rootDir = rootDir;
+
+               artifactMapping.put("org.argeo.dep.jacob", "com.jacob");
+               artifactMapping.put("org.argeo.dep.jacob.win32.x86",
+                               "com.jacob.win32.x86");
+               artifactMapping.put("org.argeo.dep.osgi.activemq",
+                               "org.apache.activemq");
+               artifactMapping.put("org.argeo.dep.osgi.activemq.optional",
+                               "org.apache.activemq.optional");
+               artifactMapping.put("org.argeo.dep.osgi.activemq.xmpp",
+                               "org.apache.activemq.xmpp");
+               artifactMapping.put("org.argeo.dep.osgi.aether", "org.eclipse.aether");
+               artifactMapping.put("org.argeo.dep.osgi.boilerpipe",
+                               "de.l3s.boilerpipe");
+               artifactMapping.put("org.argeo.dep.osgi.commons.cli",
+                               "org.apache.commons.cli");
+               artifactMapping.put("org.argeo.dep.osgi.commons.exec",
+                               "org.apache.commons.exec");
+               artifactMapping.put("org.argeo.dep.osgi.directory.shared.asn.codec",
+                               "org.apache.directory.shared.asn.codec");
+               artifactMapping.put("org.argeo.dep.osgi.drewnoakes.metadata_extractor",
+                               "com.drewnoakes.metadata_extractor");
+               artifactMapping.put("org.argeo.dep.osgi.geoapi", "org.opengis");
+               artifactMapping.put("org.argeo.dep.osgi.geotools", "org.geotools");
+               artifactMapping.put("org.argeo.dep.osgi.google.collections",
+                               "com.google.collections");
+               artifactMapping.put("org.argeo.dep.osgi.hibernatespatial",
+                               "org.hibernatespatial");
+               artifactMapping.put("org.argeo.dep.osgi.jackrabbit",
+                               "org.apache.jackrabbit");
+               artifactMapping.put("org.argeo.dep.osgi.jai.imageio",
+                               "com.sun.media.jai.imageio");
+               artifactMapping.put("org.argeo.dep.osgi.java3d", "javax.vecmath");
+               artifactMapping.put("org.argeo.dep.osgi.jcr", "javax.jcr");
+               artifactMapping.put("org.argeo.dep.osgi.jsr275", "javax.measure");
+               artifactMapping.put("org.argeo.dep.osgi.jts", "com.vividsolutions.jts");
+               artifactMapping.put("org.argeo.dep.osgi.mina.filter.ssl",
+                               "org.apache.mina.filter.ssl");
+               artifactMapping.put("org.argeo.dep.osgi.modeshape", "org.modeshape");
+               artifactMapping.put("org.argeo.dep.osgi.netcdf",
+                               "edu.ucar.unidata.netcdf");
+               artifactMapping.put("org.argeo.dep.osgi.pdfbox", "org.apache.pdfbox");
+               artifactMapping.put("org.argeo.dep.osgi.poi", "org.apache.poi");
+               artifactMapping.put("org.argeo.dep.osgi.postgis.jdbc",
+                               "org.postgis.jdbc");
+               artifactMapping.put("org.argeo.dep.osgi.springframework.ldap",
+                               "org.springframework.ldap");
+               artifactMapping.put("org.argeo.dep.osgi.tagsoup",
+                               "org.ccil.cowan.tagsoup");
+               artifactMapping.put("org.argeo.dep.osgi.tika", "org.apache.tika");
+       }
+
+       public void run() {
+               traverse(rootDir);
+       }
+
+       protected void traverse(File dir) {
+               for (File file : dir.listFiles()) {
+                       String fileName = file.getName();
+                       if (file.isDirectory() && !skipDirName(fileName)) {
+                               traverse(file);
+                       } else if (fileName.equals("pom.xml")) {
+                               processPom(file);
+                       }
+               }
+       }
+
+       protected Boolean skipDirName(String fileName) {
+               return fileName.equals(".svn") || fileName.equals("target")
+                               || fileName.equals("META-INF") || fileName.equals("src");
+       }
+
+       protected void processPom(File pomFile) {
+               try {
+                       Boolean wasChanged = false;
+                       DocumentBuilderFactory dbFactory = DocumentBuilderFactory
+                                       .newInstance();
+                       DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
+                       Document doc = dBuilder.parse(pomFile);
+                       doc.getDocumentElement().normalize();
+
+                       Element dependenciesElement = null;
+                       NodeList rootChildren = doc.getDocumentElement().getChildNodes();
+                       for (int temp = 0; temp < rootChildren.getLength(); temp++) {
+                               Node n = rootChildren.item(temp);
+                               if (n.getNodeName().equals("dependencies"))
+                                       dependenciesElement = (Element) n;
+                       }
+
+                       if (dependenciesElement != null) {
+                               stdOut("\n## " + pomFile);
+                               NodeList dependencyElements = dependenciesElement
+                                               .getElementsByTagName("dependency");
+
+                               for (int temp = 0; temp < dependencyElements.getLength(); temp++) {
+                                       Element eElement = (Element) dependencyElements.item(temp);
+                                       String groupId = getTagValue(eElement, "groupId");
+                                       String artifactId = getTagValue(eElement, "artifactId");
+                                       // stdOut(groupId + ":" + artifactId);
+
+                                       String newGroupId = null;
+                                       String newArtifactId = null;
+                                       if (groupId.startsWith("org.argeo.dep")) {
+                                               newGroupId = "org.argeo.tp";
+                                       } else if (!(groupId.startsWith("org.argeo")
+                                                       || groupId.startsWith("com.capco")
+                                                       || groupId.startsWith("com.agfa") || groupId
+                                                       .startsWith("org.ibboost"))) {
+                                               newGroupId = "org.argeo.tp";
+                                       }
+
+                                       if (artifactMapping.containsKey(artifactId)) {
+                                               newArtifactId = artifactMapping.get(artifactId);
+                                       } else if (artifactId.startsWith(SPRING_SOURCE_PREFIX)
+                                                       && !artifactId.equals(SPRING_SOURCE_PREFIX
+                                                                       + ".json")) {
+                                               newArtifactId = artifactId
+                                                               .substring(SPRING_SOURCE_PREFIX.length() + 1);
+                                       }
+
+                                       // modify
+                                       if (newGroupId != null || newArtifactId != null) {
+                                               if (newGroupId == null)
+                                                       newGroupId = groupId;
+                                               if (newArtifactId == null)
+                                                       newArtifactId = artifactId;
+                                               stdOut(groupId + ":" + artifactId + " => " + newGroupId
+                                                               + ":" + newArtifactId);
+                                               setTagValue(eElement, "groupId", newGroupId);
+                                               setTagValue(eElement, "artifactId", newArtifactId);
+                                               wasChanged = true;
+                                       }
+                               }
+                       }
+
+                       if (wasChanged) {
+                               // pomFile.renameTo(new File(pomFile.getParentFile(),
+                               // "pom-old.xml"));
+                               // save in place
+                               Source source = new DOMSource(doc);
+                               Result result = new StreamResult(pomFile);
+                               Transformer xformer = TransformerFactory.newInstance()
+                                               .newTransformer();
+                               xformer.transform(source, result);
+                       }
+               } catch (Exception e) {
+                       throw new RuntimeException("Cannot process " + pomFile, e);
+               }
+
+       }
+
+       private String getTagValue(Element eElement, String sTag) {
+               NodeList nList = eElement.getElementsByTagName(sTag);
+               if (nList.getLength() > 0) {
+                       NodeList nlList = nList.item(0).getChildNodes();
+                       Node nValue = (Node) nlList.item(0);
+                       return nValue.getNodeValue();
+               } else
+                       return null;
+       }
+
+       private void setTagValue(Element eElement, String sTag, String value) {
+               NodeList nList = eElement.getElementsByTagName(sTag);
+               if (nList.getLength() > 0) {
+                       NodeList nlList = nList.item(0).getChildNodes();
+                       Node nValue = (Node) nlList.item(0);
+                       nValue.setNodeValue(value);
+               }
+       }
+
+       public static void stdOut(Object obj) {
+               System.out.println(obj);
+       }
+
+       public static void main(String argv[]) {
+               new ConvertPoms_01_03(argv[0]).run();
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/maven/GenerateBinaries.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/maven/GenerateBinaries.java
new file mode 100644 (file)
index 0000000..095b371
--- /dev/null
@@ -0,0 +1,537 @@
+package org.argeo.slc.repo.maven;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.TreeSet;
+
+import javax.jcr.Credentials;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrMonitor;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.argeo.slc.repo.ArtifactIndexer;
+import org.argeo.slc.repo.RepoConstants;
+import org.argeo.slc.repo.RepoUtils;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.osgi.framework.Version;
+
+/**
+ * Generates binaries-, sources- and sdk-version.pom artifacts for a given
+ * group.
+ */
+public class GenerateBinaries implements Runnable, SlcNames {
+       private final static CmsLog log = CmsLog.getLog(GenerateBinaries.class);
+
+       // Connection info
+       private Repository repository;
+       private Credentials credentials;
+       private String workspace;
+
+       // Business info
+       private String groupId;
+       private String parentPomCoordinates;
+       private String version = null;
+
+       // Constants
+       private String artifactBasePath = RepoConstants.DEFAULT_ARTIFACTS_BASE_PATH;
+       private List<String> excludedSuffixes = new ArrayList<String>();
+
+       // Indexes
+       private Set<Artifact> binaries = new TreeSet<Artifact>(new ArtifactIdComparator());
+       private Set<Artifact> sources = new TreeSet<Artifact>(new ArtifactIdComparator());
+
+       // local cache
+       private ArtifactIndexer artifactIndexer = new ArtifactIndexer();
+       private Node allArtifactsHighestVersion;
+
+       public void run() {
+               Session session = null;
+               try {
+                       session = repository.login(credentials, workspace);
+                       Node groupNode = session.getNode(MavenConventionsUtils.groupPath(artifactBasePath, groupId));
+                       internalPreProcessing(groupNode, null);
+                       internalProcessing(groupNode, null);
+               } catch (Exception e) {
+                       throw new SlcException("Cannot normalize group " + groupId + " in " + workspace, e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+       /**
+        * Generates binaries-, sources- and sdk-version.pom artifacts for the given
+        * version (or the highest of all children version if none is precised).
+        * 
+        * By default, it includes each latest version of all artifact of this group.
+        * 
+        * The 3 generated artifacts are then marked as modular distributions and
+        * indexed.
+        */
+       public static void processGroupNode(Node groupNode, String version, JcrMonitor monitor) throws RepositoryException {
+               // TODO set artifactsBase based on group node
+               GenerateBinaries gb = new GenerateBinaries();
+               String groupId = groupNode.getProperty(SlcNames.SLC_GROUP_BASE_ID).getString();
+               gb.setGroupId(groupId);
+               gb.setVersion(version);
+               // TODO use already done pre-processing
+               gb.internalPreProcessing(groupNode, monitor);
+               gb.internalProcessing(groupNode, monitor);
+       }
+
+       /** Only builds local indexes. Does not change anything in the local Session */
+       public static GenerateBinaries preProcessGroupNode(Node groupNode, JcrMonitor monitor) throws RepositoryException {
+               // TODO set artifactsBase based on group node
+               GenerateBinaries gb = new GenerateBinaries();
+               String groupId = groupNode.getProperty(SlcNames.SLC_GROUP_BASE_ID).getString();
+               gb.setGroupId(groupId);
+               // gb.setVersion(version);
+               // gb.setOverridePoms(overridePoms);
+               gb.internalPreProcessing(groupNode, monitor);
+               return gb;
+       }
+
+       // exposes indexes. to display results of the pre-processing phase.
+       public Set<Artifact> getBinaries() {
+               return binaries;
+       }
+
+       public Artifact getHighestArtifactVersion() throws RepositoryException {
+               return allArtifactsHighestVersion == null ? null : RepoUtils.asArtifact(allArtifactsHighestVersion);
+       }
+
+       // //////////////////////////////////////
+       // INTERNAL METHODS
+
+       /**
+        * Browse all children of a Node considered as a folder that follows Aether
+        * conventions i.e that has Aether's artifact base as children.
+        * 
+        * Each of such child contains a set of Aether artifact versions. This methods
+        * build the binaries {@code Set<Artifact>} and other indexes. It does not
+        * impact the
+        */
+       protected void internalPreProcessing(Node groupNode, JcrMonitor monitor) throws RepositoryException {
+               if (monitor != null)
+                       monitor.subTask("Pre processing group " + groupId);
+
+               // Process all direct children nodes,
+               // gathering latest versions of each artifact
+               allArtifactsHighestVersion = null;
+
+               aBases: for (NodeIterator aBases = groupNode.getNodes(); aBases.hasNext();) {
+                       Node aBase = aBases.nextNode();
+                       if (aBase.isNodeType(SlcTypes.SLC_ARTIFACT_BASE)) {
+                               Node highestAVersion = getArtifactLatestVersion(aBase);
+                               if (highestAVersion == null)
+                                       continue aBases;
+                               else {
+                                       // retrieve relevant child node
+                                       // Information is stored on the NT_FILE child node.
+                                       for (NodeIterator files = highestAVersion.getNodes(); files.hasNext();) {
+                                               Node file = files.nextNode();
+                                               if (file.isNodeType(SlcTypes.SLC_BUNDLE_ARTIFACT)) {
+                                                       if (log.isDebugEnabled())
+                                                               log.debug("Pre-Processing " + file.getName());
+                                                       preProcessBundleArtifact(file);
+                                               }
+                                       }
+                               }
+                       }
+               }
+               // if (log.isDebugEnabled()) {
+               // int bundleCount = symbolicNamesToNodes.size();
+               // log.debug("" + bundleCount + " bundles have been indexed for "
+               // + groupId);
+               // }
+       }
+
+       /** Does the real job : writes JCR META-DATA and generates binaries */
+       protected void internalProcessing(Node groupNode, JcrMonitor monitor) throws RepositoryException {
+               if (monitor != null)
+                       monitor.subTask("Processing group " + groupId);
+
+               Session session = groupNode.getSession();
+
+               // if version not set or empty, use the highest version
+               // useful when indexing a product maven repository where
+               // all artifacts have the same version for a given release
+               // => the version can then be left empty
+               if (version == null || version.trim().equals(""))
+                       if (allArtifactsHighestVersion != null)
+                               version = allArtifactsHighestVersion.getProperty(SLC_ARTIFACT_VERSION).getString();
+                       else
+                               throw new SlcException("Group version " + version + " is empty.");
+
+               // int bundleCount = symbolicNamesToNodes.size();
+               // int count = 1;
+               // for (Node bundleNode : symbolicNamesToNodes.values()) {
+               // if (log.isDebugEnabled())
+               // log.debug("Processing " + bundleNode.getName() + " ( " + count
+               // + "/" + bundleCount + " )");
+               //
+               // // processBundleArtifact(bundleNode);
+               // // bundleNode.getSession().save();
+               // count++;
+               // }
+
+               // indexes
+               Set<Artifact> indexes = new TreeSet<Artifact>(new ArtifactIdComparator());
+
+               Artifact indexArtifact;
+               indexArtifact = writeIndex(session, RepoConstants.BINARIES_ARTIFACT_ID, binaries);
+               indexes.add(indexArtifact);
+
+               indexArtifact = writeIndex(session, RepoConstants.SOURCES_ARTIFACT_ID, sources);
+               indexes.add(indexArtifact);
+
+               // sdk
+               writeIndex(session, RepoConstants.SDK_ARTIFACT_ID, indexes);
+
+               if (monitor != null)
+                       monitor.worked(1);
+       }
+
+       protected void preProcessBundleArtifact(Node bundleNode) throws RepositoryException {
+
+               String symbolicName = JcrUtils.get(bundleNode, SLC_SYMBOLIC_NAME);
+               // Sanity check.
+               if (symbolicName == null)
+                       log.warn("Symbolic name is null for bundle " + bundleNode);
+
+               // Manage source bundles
+               if (symbolicName.endsWith(".source")) {
+                       // TODO make a shared node with classifier 'sources'?
+                       String bundleName = RepoUtils.extractBundleNameFromSourceName(symbolicName);
+                       for (String excludedSuffix : excludedSuffixes) {
+                               if (bundleName.endsWith(excludedSuffix))
+                                       return;// skip adding to sources
+                       }
+                       sources.add(RepoUtils.asArtifact(bundleNode));
+                       return;
+               }
+
+               // // Build indexes
+               // NodeIterator exportPackages = bundleNode.getNodes(SLC_
+               // + Constants.EXPORT_PACKAGE);
+               // while (exportPackages.hasNext()) {
+               // Node exportPackage = exportPackages.nextNode();
+               // String pkg = JcrUtils.get(exportPackage, SLC_NAME);
+               // packagesToSymbolicNames.put(pkg, symbolicName);
+               // }
+               //
+               // symbolicNamesToNodes.put(symbolicName, bundleNode);
+               // for (String excludedSuffix : excludedSuffixes) {
+               // if (symbolicName.endsWith(excludedSuffix))
+               // return;// skip adding to binaries
+               // }
+
+               binaries.add(RepoUtils.asArtifact(bundleNode));
+
+               // Extra check. to remove
+               if (bundleNode.getSession().hasPendingChanges())
+                       throw new SlcException("Pending changes in the session, " + "this should not be true here.");
+       }
+
+       // protected void processBundleArtifact(Node bundleNode)
+       // throws RepositoryException {
+       // Node artifactFolder = bundleNode.getParent();
+       // String baseName = FilenameUtils.getBaseName(bundleNode.getName());
+       //
+       // // pom
+       // String pomName = baseName + ".pom";
+       // if (artifactFolder.hasNode(pomName) && !overridePoms)
+       // return;// skip
+       //
+       // String pom = generatePomForBundle(bundleNode);
+       // Node pomNode = JcrUtils.copyBytesAsFile(artifactFolder, pomName,
+       // pom.getBytes());
+       // // checksum
+       // String bundleSha = JcrUtils.checksumFile(bundleNode, "SHA-1");
+       // JcrUtils.copyBytesAsFile(artifactFolder,
+       // bundleNode.getName() + ".sha1", bundleSha.getBytes());
+       // String pomSha = JcrUtils.checksumFile(pomNode, "SHA-1");
+       // JcrUtils.copyBytesAsFile(artifactFolder, pomNode.getName() + ".sha1",
+       // pomSha.getBytes());
+       // }
+
+       // ////////////////////
+       // LOCAL WRITERS
+       //
+
+       private Artifact writeIndex(Session session, String artifactId, Set<Artifact> artifacts)
+                       throws RepositoryException {
+               Artifact artifact = new DefaultArtifact(groupId, artifactId, "pom", version);
+               Artifact parentArtifact = parentPomCoordinates != null ? new DefaultArtifact(parentPomCoordinates) : null;
+               String pom = MavenConventionsUtils.artifactsAsDependencyPom(artifact, artifacts, parentArtifact);
+               Node node = RepoUtils.copyBytesAsArtifact(session.getNode(artifactBasePath), artifact, pom.getBytes());
+               artifactIndexer.index(node);
+
+               // TODO factorize
+               String pomSha = JcrUtils.checksumFile(node, "SHA-1");
+               JcrUtils.copyBytesAsFile(node.getParent(), node.getName() + ".sha1", pomSha.getBytes());
+               String pomMd5 = JcrUtils.checksumFile(node, "MD5");
+               JcrUtils.copyBytesAsFile(node.getParent(), node.getName() + ".md5", pomMd5.getBytes());
+               session.save();
+               return artifact;
+       }
+
+       // Helpers
+       private Node getArtifactLatestVersion(Node artifactBase) {
+               try {
+                       Node highestAVersion = null;
+                       for (NodeIterator aVersions = artifactBase.getNodes(); aVersions.hasNext();) {
+                               Node aVersion = aVersions.nextNode();
+                               if (aVersion.isNodeType(SlcTypes.SLC_ARTIFACT_VERSION_BASE)) {
+                                       if (highestAVersion == null) {
+                                               highestAVersion = aVersion;
+                                               if (allArtifactsHighestVersion == null)
+                                                       allArtifactsHighestVersion = aVersion;
+                                               // Correctly handle following arrival order:
+                                               // Name1 - V1, name2 - V3
+                                               else {
+                                                       Version cachedHighestVersion = extractOsgiVersion(allArtifactsHighestVersion);
+                                                       Version currVersion = extractOsgiVersion(aVersion);
+                                                       if (currVersion.compareTo(cachedHighestVersion) > 0)
+                                                               allArtifactsHighestVersion = aVersion;
+                                               }
+                                       } else {
+                                               Version currVersion = extractOsgiVersion(aVersion);
+                                               Version currentHighestVersion = extractOsgiVersion(highestAVersion);
+                                               if (currVersion.compareTo(currentHighestVersion) > 0) {
+                                                       highestAVersion = aVersion;
+                                               }
+                                               if (currVersion.compareTo(extractOsgiVersion(allArtifactsHighestVersion)) > 0) {
+                                                       allArtifactsHighestVersion = aVersion;
+                                               }
+                                       }
+
+                               }
+                       }
+                       return highestAVersion;
+               } catch (RepositoryException re) {
+                       throw new SlcException("Unable to get latest version for node " + artifactBase, re);
+               }
+       }
+
+       private Version extractOsgiVersion(Node artifactVersion) throws RepositoryException {
+               String rawVersion = artifactVersion.getProperty(SLC_ARTIFACT_VERSION).getString();
+               String cleanVersion = rawVersion.replace("-SNAPSHOT", ".SNAPSHOT");
+               Version osgiVersion = null;
+               // log invalid version value to enable tracking them
+               try {
+                       osgiVersion = new Version(cleanVersion);
+               } catch (IllegalArgumentException e) {
+                       log.error("Version string " + cleanVersion + " is invalid ");
+                       String twickedVersion = twickInvalidVersion(cleanVersion);
+                       osgiVersion = new Version(twickedVersion);
+                       log.error("Using " + twickedVersion + " instead");
+                       // throw e;
+               }
+               return osgiVersion;
+       }
+
+       private String twickInvalidVersion(String tmpVersion) {
+               String[] tokens = tmpVersion.split("\\.");
+               if (tokens.length == 3 && tokens[2].lastIndexOf("-") > 0) {
+                       String newSuffix = tokens[2].replaceFirst("-", ".");
+                       tmpVersion = tmpVersion.replaceFirst(tokens[2], newSuffix);
+               } else if (tokens.length > 4) {
+                       // FIXME manually remove other "."
+                       StringTokenizer st = new StringTokenizer(tmpVersion, ".", true);
+                       StringBuilder builder = new StringBuilder();
+                       // Major
+                       builder.append(st.nextToken()).append(st.nextToken());
+                       // Minor
+                       builder.append(st.nextToken()).append(st.nextToken());
+                       // Micro
+                       builder.append(st.nextToken()).append(st.nextToken());
+                       // Qualifier
+                       builder.append(st.nextToken());
+                       while (st.hasMoreTokens()) {
+                               // consume delimiter
+                               st.nextToken();
+                               if (st.hasMoreTokens())
+                                       builder.append("-").append(st.nextToken());
+                       }
+                       tmpVersion = builder.toString();
+               }
+               return tmpVersion;
+       }
+
+       // private String generatePomForBundle(Node n) throws RepositoryException {
+       // String ownSymbolicName = JcrUtils.get(n, SLC_SYMBOLIC_NAME);
+       //
+       // StringBuffer p = new StringBuffer();
+       //
+       // // XML header
+       // p.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
+       // p.append("<project xmlns=\"http://maven.apache.org/POM/4.0.0\"
+       // xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"
+       // xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0
+       // http://maven.apache.org/maven-v4_0_0.xsd\">\n");
+       // p.append("<modelVersion>4.0.0</modelVersion>");
+       //
+       // // Artifact
+       // p.append("<groupId>").append(JcrUtils.get(n, SLC_GROUP_ID))
+       // .append("</groupId>\n");
+       // p.append("<artifactId>").append(JcrUtils.get(n, SLC_ARTIFACT_ID))
+       // .append("</artifactId>\n");
+       // p.append("<version>").append(JcrUtils.get(n, SLC_ARTIFACT_VERSION))
+       // .append("</version>\n");
+       // p.append("<packaging>pom</packaging>\n");
+       // if (n.hasProperty(SLC_ + Constants.BUNDLE_NAME))
+       // p.append("<name>")
+       // .append(JcrUtils.get(n, SLC_ + Constants.BUNDLE_NAME))
+       // .append("</name>\n");
+       // if (n.hasProperty(SLC_ + Constants.BUNDLE_DESCRIPTION))
+       // p.append("<description>")
+       // .append(JcrUtils
+       // .get(n, SLC_ + Constants.BUNDLE_DESCRIPTION))
+       // .append("</description>\n");
+       //
+       // // Dependencies
+       // Set<String> dependenciesSymbolicNames = new TreeSet<String>();
+       // Set<String> optionalSymbolicNames = new TreeSet<String>();
+       // NodeIterator importPackages = n.getNodes(SLC_
+       // + Constants.IMPORT_PACKAGE);
+       // while (importPackages.hasNext()) {
+       // Node importPackage = importPackages.nextNode();
+       // String pkg = JcrUtils.get(importPackage, SLC_NAME);
+       // if (packagesToSymbolicNames.containsKey(pkg)) {
+       // String dependencySymbolicName = packagesToSymbolicNames
+       // .get(pkg);
+       // if (JcrUtils.check(importPackage, SLC_OPTIONAL))
+       // optionalSymbolicNames.add(dependencySymbolicName);
+       // else
+       // dependenciesSymbolicNames.add(dependencySymbolicName);
+       // } else {
+       // if (!JcrUtils.check(importPackage, SLC_OPTIONAL)
+       // && !systemPackages.contains(pkg))
+       // log.warn("No bundle found for pkg " + pkg);
+       // }
+       // }
+       //
+       // if (n.hasNode(SLC_ + Constants.FRAGMENT_HOST)) {
+       // String fragmentHost = JcrUtils.get(
+       // n.getNode(SLC_ + Constants.FRAGMENT_HOST),
+       // SLC_SYMBOLIC_NAME);
+       // dependenciesSymbolicNames.add(fragmentHost);
+       // }
+       //
+       // // TODO require bundles
+       //
+       // List<Node> dependencyNodes = new ArrayList<Node>();
+       // for (String depSymbName : dependenciesSymbolicNames) {
+       // if (depSymbName.equals(ownSymbolicName))
+       // continue;// skip self
+       //
+       // if (symbolicNamesToNodes.containsKey(depSymbName))
+       // dependencyNodes.add(symbolicNamesToNodes.get(depSymbName));
+       // else
+       // log.warn("Could not find node for " + depSymbName);
+       // }
+       // List<Node> optionalDependencyNodes = new ArrayList<Node>();
+       // for (String depSymbName : optionalSymbolicNames) {
+       // if (symbolicNamesToNodes.containsKey(depSymbName))
+       // optionalDependencyNodes.add(symbolicNamesToNodes
+       // .get(depSymbName));
+       // else
+       // log.warn("Could not find node for " + depSymbName);
+       // }
+       //
+       // p.append("<dependencies>\n");
+       // for (Node dependencyNode : dependencyNodes) {
+       // p.append("<dependency>\n");
+       // p.append("\t<groupId>")
+       // .append(JcrUtils.get(dependencyNode, SLC_GROUP_ID))
+       // .append("</groupId>\n");
+       // p.append("\t<artifactId>")
+       // .append(JcrUtils.get(dependencyNode, SLC_ARTIFACT_ID))
+       // .append("</artifactId>\n");
+       // p.append("</dependency>\n");
+       // }
+       //
+       // if (optionalDependencyNodes.size() > 0)
+       // p.append("<!-- OPTIONAL -->\n");
+       // for (Node dependencyNode : optionalDependencyNodes) {
+       // p.append("<dependency>\n");
+       // p.append("\t<groupId>")
+       // .append(JcrUtils.get(dependencyNode, SLC_GROUP_ID))
+       // .append("</groupId>\n");
+       // p.append("\t<artifactId>")
+       // .append(JcrUtils.get(dependencyNode, SLC_ARTIFACT_ID))
+       // .append("</artifactId>\n");
+       // p.append("\t<optional>true</optional>\n");
+       // p.append("</dependency>\n");
+       // }
+       // p.append("</dependencies>\n");
+       //
+       // // Dependency management
+       // p.append("<dependencyManagement>\n");
+       // p.append("<dependencies>\n");
+       // p.append("<dependency>\n");
+       // p.append("\t<groupId>").append(groupId).append("</groupId>\n");
+       // p.append("\t<artifactId>")
+       // .append(ownSymbolicName.endsWith(".source") ?
+       // RepoConstants.SOURCES_ARTIFACT_ID
+       // : RepoConstants.BINARIES_ARTIFACT_ID)
+       // .append("</artifactId>\n");
+       // p.append("\t<version>").append(version).append("</version>\n");
+       // p.append("\t<type>pom</type>\n");
+       // p.append("\t<scope>import</scope>\n");
+       // p.append("</dependency>\n");
+       // p.append("</dependencies>\n");
+       // p.append("</dependencyManagement>\n");
+       //
+       // p.append("</project>\n");
+       // return p.toString();
+       // }
+
+       /* SETTERS */
+       public void setRepository(Repository repository) {
+               this.repository = repository;
+       }
+
+       public void setCredentials(Credentials credentials) {
+               this.credentials = credentials;
+       }
+
+       public void setWorkspace(String workspace) {
+               this.workspace = workspace;
+       }
+
+       public void setGroupId(String groupId) {
+               this.groupId = groupId;
+       }
+
+       public void setParentPomCoordinates(String parentPomCoordinates) {
+               this.parentPomCoordinates = parentPomCoordinates;
+       }
+
+       public void setArtifactBasePath(String artifactBasePath) {
+               this.artifactBasePath = artifactBasePath;
+       }
+
+       public void setVersion(String version) {
+               this.version = version;
+       }
+
+       public void setExcludedSuffixes(List<String> excludedSuffixes) {
+               this.excludedSuffixes = excludedSuffixes;
+       }
+
+       public void setArtifactIndexer(ArtifactIndexer artifactIndexer) {
+               this.artifactIndexer = artifactIndexer;
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/maven/IndexDistribution.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/maven/IndexDistribution.java
new file mode 100644 (file)
index 0000000..fec2716
--- /dev/null
@@ -0,0 +1,129 @@
+package org.argeo.slc.repo.maven;
+
+import java.io.File;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.argeo.slc.repo.RepoConstants;
+import org.eclipse.aether.artifact.Artifact;
+
+/** Create a distribution node from a set of artifacts */
+public class IndexDistribution implements Runnable {
+       private final static CmsLog log = CmsLog.getLog(IndexDistribution.class);
+       private Repository repository;
+       private String workspace;
+
+       private String artifactBasePath = RepoConstants.DEFAULT_ARTIFACTS_BASE_PATH;
+       private String distributionsBasePath = RepoConstants.DISTRIBUTIONS_BASE_PATH;
+       private String distributionName;
+
+       public void run() {
+               // TODO populate
+               Set<Artifact> artifacts = new HashSet<Artifact>();
+
+               // sync
+               Session session = null;
+               try {
+                       session = repository.login(workspace);
+                       syncDistribution(session, artifacts);
+               } catch (Exception e) {
+                       throw new SlcException("Cannot import distribution", e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+       protected void syncDistribution(Session jcrSession, Set<Artifact> artifacts) {
+               Long begin = System.currentTimeMillis();
+               try {
+                       JcrUtils.mkdirs(jcrSession, distributionsBasePath + '/'
+                                       + distributionName);
+                       artifacts: for (Artifact artifact : artifacts) {
+                               File file = artifact.getFile();
+                               if (file == null) {
+                                       file = MavenConventionsUtils.artifactToFile(artifact);
+                                       if (!file.exists()) {
+                                               log.warn("Generated file " + file + " for " + artifact
+                                                               + " does not exist");
+                                               continue artifacts;
+                                       }
+                               }
+
+                               try {
+                                       String parentPath = artifactBasePath
+                                                       + (artifactBasePath.endsWith("/") ? "" : "/")
+                                                       + artifactParentPath(artifact);
+                                       Node parentNode = jcrSession.getNode(parentPath);
+                                       Node fileNode = parentNode.getNode(file.getName());
+
+                                       if (fileNode.hasProperty(SlcNames.SLC_SYMBOLIC_NAME)) {
+                                               String distPath = bundleDistributionPath(fileNode);
+                                               if (!jcrSession.itemExists(distPath)
+                                                               && fileNode
+                                                                               .isNodeType(SlcTypes.SLC_BUNDLE_ARTIFACT))
+                                                       jcrSession.getWorkspace().clone(
+                                                                       jcrSession.getWorkspace().getName(),
+                                                                       fileNode.getPath(), distPath, false);
+                                               if (log.isDebugEnabled())
+                                                       log.debug("Indexed " + fileNode);
+                                       }
+                               } catch (Exception e) {
+                                       log.error("Could not index " + artifact, e);
+                                       jcrSession.refresh(false);
+                                       throw e;
+                               }
+                       }
+
+                       Long duration = (System.currentTimeMillis() - begin) / 1000;
+                       if (log.isDebugEnabled())
+                               log.debug("Indexed distribution in " + duration + "s");
+               } catch (Exception e) {
+                       throw new SlcException("Cannot synchronize distribution", e);
+               }
+       }
+
+       private String artifactParentPath(Artifact artifact) {
+               return artifact.getGroupId().replace('.', '/') + '/'
+                               + artifact.getArtifactId() + '/' + artifact.getVersion();
+       }
+
+       private String bundleDistributionPath(Node fileNode) {
+               try {
+                       return distributionsBasePath
+                                       + '/'
+                                       + distributionName
+                                       + '/'
+                                       + fileNode.getProperty(SlcNames.SLC_SYMBOLIC_NAME)
+                                                       .getString()
+                                       + '_'
+                                       + fileNode.getProperty(SlcNames.SLC_BUNDLE_VERSION)
+                                                       .getString();
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot create distribution path for "
+                                       + fileNode, e);
+               }
+       }
+
+       public void setDistributionName(String distributionName) {
+               this.distributionName = distributionName;
+       }
+
+       public void setRepository(Repository repository) {
+               this.repository = repository;
+       }
+
+       public void setWorkspace(String workspace) {
+               this.workspace = workspace;
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/maven/MavenConventionsUtils.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/maven/MavenConventionsUtils.java
new file mode 100644 (file)
index 0000000..f3e359f
--- /dev/null
@@ -0,0 +1,203 @@
+package org.argeo.slc.repo.maven;
+
+import java.io.File;
+import java.util.Set;
+
+import org.argeo.api.cms.CmsLog;
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * Static utilities around Maven which are NOT using the Maven APIs (conventions
+ * based).
+ */
+public class MavenConventionsUtils {
+       private final static CmsLog log = CmsLog.getLog(MavenConventionsUtils.class);
+
+       /**
+        * Path to the file identified by this artifact <b>without</b> using Maven
+        * APIs (convention based). Default location of repository
+        * (~/.m2/repository) is used here.
+        * 
+        * @see MavenConventionsUtils#artifactToFile(String, Artifact)
+        */
+       public static File artifactToFile(Artifact artifact) {
+               return artifactToFile(System.getProperty("user.home") + File.separator + ".m2" + File.separator + "repository",
+                               artifact);
+       }
+
+       /**
+        * Path to the file identified by this artifact <b>without</b> using Maven
+        * APIs (convention based).
+        * 
+        * @param repositoryPath
+        *            path to the related local repository location
+        * @param artifact
+        *            the artifact
+        */
+       public static File artifactToFile(String repositoryPath, Artifact artifact) {
+               return new File(repositoryPath + File.separator + artifact.getGroupId().replace('.', File.separatorChar)
+                               + File.separator + artifact.getArtifactId() + File.separator + artifact.getVersion() + File.separator
+                               + artifactFileName(artifact)).getAbsoluteFile();
+       }
+
+       /** The file name of this artifact when stored */
+       public static String artifactFileName(Artifact artifact) {
+               return artifact.getArtifactId() + '-' + artifact.getVersion()
+                               + (artifact.getClassifier().equals("") ? "" : '-' + artifact.getClassifier()) + '.'
+                               + artifact.getExtension();
+       }
+
+       /** Absolute path to the file */
+       public static String artifactPath(String artifactBasePath, Artifact artifact) {
+               return artifactParentPath(artifactBasePath, artifact) + '/' + artifactFileName(artifact);
+       }
+
+       /** Absolute path to the file */
+       public static String artifactUrl(String repoUrl, Artifact artifact) {
+               if (repoUrl.endsWith("/"))
+                       return repoUrl + artifactPath("/", artifact).substring(1);
+               else
+                       return repoUrl + artifactPath("/", artifact);
+       }
+
+       /** Absolute path to the directories where the files will be stored */
+       public static String artifactParentPath(String artifactBasePath, Artifact artifact) {
+               return artifactBasePath + (artifactBasePath.endsWith("/") ? "" : "/") + artifactParentPath(artifact);
+       }
+
+       /** Absolute path to the directory of this group */
+       public static String groupPath(String artifactBasePath, String groupId) {
+               return artifactBasePath + (artifactBasePath.endsWith("/") ? "" : "/") + groupId.replace('.', '/');
+       }
+
+       /** Relative path to the directories where the files will be stored */
+       public static String artifactParentPath(Artifact artifact) {
+               return artifact.getGroupId().replace('.', '/') + '/' + artifact.getArtifactId() + '/'
+                               + artifact.getBaseVersion();
+       }
+
+       public static String artifactsAsDependencyPom(Artifact pomArtifact, Set<Artifact> artifacts, Artifact parent) {
+               StringBuffer p = new StringBuffer();
+
+               // XML header
+               p.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
+               p.append(
+                               "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n");
+               p.append("<modelVersion>4.0.0</modelVersion>\n");
+
+               // Artifact
+               if (parent != null) {
+                       p.append("<parent>\n");
+                       p.append("<groupId>").append(parent.getGroupId()).append("</groupId>\n");
+                       p.append("<artifactId>").append(parent.getArtifactId()).append("</artifactId>\n");
+                       p.append("<version>").append(parent.getVersion()).append("</version>\n");
+                       p.append("</parent>\n");
+               }
+               p.append("<groupId>").append(pomArtifact.getGroupId()).append("</groupId>\n");
+               p.append("<artifactId>").append(pomArtifact.getArtifactId()).append("</artifactId>\n");
+               p.append("<version>").append(pomArtifact.getVersion()).append("</version>\n");
+               p.append("<packaging>pom</packaging>\n");
+
+               // Dependencies
+               p.append("<dependencies>\n");
+               for (Artifact a : artifacts) {
+                       p.append("\t<dependency>");
+                       p.append("<artifactId>").append(a.getArtifactId()).append("</artifactId>");
+                       p.append("<groupId>").append(a.getGroupId()).append("</groupId>");
+                       if (!a.getExtension().equals("jar"))
+                               p.append("<type>").append(a.getExtension()).append("</type>");
+                       p.append("</dependency>\n");
+               }
+               p.append("</dependencies>\n");
+
+               // Dependency management
+               p.append("<dependencyManagement>\n");
+               p.append("<dependencies>\n");
+               for (Artifact a : artifacts) {
+                       p.append("\t<dependency>");
+                       p.append("<artifactId>").append(a.getArtifactId()).append("</artifactId>");
+                       p.append("<version>").append(a.getVersion()).append("</version>");
+                       p.append("<groupId>").append(a.getGroupId()).append("</groupId>");
+                       if (a.getExtension().equals("pom")) {
+                               p.append("<type>").append(a.getExtension()).append("</type>");
+                               p.append("<scope>import</scope>");
+                       }
+                       p.append("</dependency>\n");
+               }
+               p.append("</dependencies>\n");
+               p.append("</dependencyManagement>\n");
+
+               // Repositories
+               // p.append("<repositories>\n");
+               // p.append("<repository><id>argeo</id><url>http://maven.argeo.org/argeo</url></repository>\n");
+               // p.append("</repositories>\n");
+
+               p.append("</project>\n");
+               return p.toString();
+       }
+
+//     /**
+//      * Directly parses Maven POM XML format in order to find all artifacts
+//      * references under the dependency and dependencyManagement tags. This is
+//      * meant to migrate existing pom registering a lot of artifacts, not to
+//      * replace Maven resolving.
+//      */
+//     public static void gatherPomDependencies(AetherTemplate aetherTemplate, Set<Artifact> artifacts,
+//                     Artifact pomArtifact) {
+//             if (log.isDebugEnabled())
+//                     log.debug("Gather dependencies for " + pomArtifact);
+//
+//             try {
+//                     File file = aetherTemplate.getResolvedFile(pomArtifact);
+//                     DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+//                     Document doc = documentBuilder.parse(file);
+//
+//                     // properties
+//                     Properties props = new Properties();
+//                     props.setProperty("project.version", pomArtifact.getBaseVersion());
+//                     NodeList properties = doc.getElementsByTagName("properties");
+//                     if (properties.getLength() > 0) {
+//                             NodeList propertiesElems = properties.item(0).getChildNodes();
+//                             for (int i = 0; i < propertiesElems.getLength(); i++) {
+//                                     if (propertiesElems.item(i) instanceof Element) {
+//                                             Element property = (Element) propertiesElems.item(i);
+//                                             props.put(property.getNodeName(), property.getTextContent());
+//                                     }
+//                             }
+//                     }
+//
+//                     // dependencies (direct and dependencyManagement)
+//                     NodeList dependencies = doc.getElementsByTagName("dependency");
+//                     for (int i = 0; i < dependencies.getLength(); i++) {
+//                             Element dependency = (Element) dependencies.item(i);
+//                             String groupId = dependency.getElementsByTagName("groupId").item(0).getTextContent().trim();
+//                             String artifactId = dependency.getElementsByTagName("artifactId").item(0).getTextContent().trim();
+//                             String version = dependency.getElementsByTagName("version").item(0).getTextContent().trim();
+//                             if (version.startsWith("${")) {
+//                                     String versionKey = version.substring(0, version.length() - 1).substring(2);
+//                                     if (!props.containsKey(versionKey))
+//                                             throw new SlcException("Cannot interpret version " + version);
+//                                     version = props.getProperty(versionKey);
+//                             }
+//                             NodeList scopes = dependency.getElementsByTagName("scope");
+//                             if (scopes.getLength() > 0 && scopes.item(0).getTextContent().equals("import")) {
+//                                     // recurse
+//                                     gatherPomDependencies(aetherTemplate, artifacts,
+//                                                     new DefaultArtifact(groupId, artifactId, "pom", version));
+//                             } else {
+//                                     // TODO: deal with scope?
+//                                     // TODO: deal with type
+//                                     String type = "jar";
+//                                     Artifact artifact = new DefaultArtifact(groupId, artifactId, type, version);
+//                                     artifacts.add(artifact);
+//                             }
+//                     }
+//             } catch (Exception e) {
+//                     throw new SlcException("Cannot process " + pomArtifact, e);
+//             }
+//     }
+
+       /** Prevent instantiation */
+       private MavenConventionsUtils() {
+       }
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/maven/MavenProxyServiceImpl.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/maven/MavenProxyServiceImpl.java
new file mode 100644 (file)
index 0000000..caee12b
--- /dev/null
@@ -0,0 +1,98 @@
+package org.argeo.slc.repo.maven;
+
+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 javax.jcr.nodetype.NodeType;
+import javax.jcr.security.AccessControlException;
+import javax.jcr.security.Privilege;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.ArgeoNames;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.jcr.proxy.AbstractUrlProxy;
+import org.argeo.slc.SlcConstants;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.argeo.slc.repo.MavenProxyService;
+import org.argeo.slc.repo.RepoConstants;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/** Synchronises the node repository with remote Maven repositories */
+public class MavenProxyServiceImpl extends AbstractUrlProxy implements MavenProxyService, ArgeoNames, SlcNames {
+       private final static CmsLog log = CmsLog.getLog(MavenProxyServiceImpl.class);
+
+       private List<RemoteRepository> defaultRepositories = new ArrayList<RemoteRepository>();
+
+       /** Initialises the artifacts area. */
+       @Override
+       protected void beforeInitSessionSave(Session session) throws RepositoryException {
+               JcrUtils.addPrivilege(session, "/", SlcConstants.USER_ANONYMOUS, Privilege.JCR_READ);
+               try {
+                       JcrUtils.addPrivilege(session, "/", SlcConstants.ROLE_SLC, Privilege.JCR_ALL);
+               } catch (AccessControlException e) {
+                       if (log.isTraceEnabled())
+                               log.trace("Cannot give jcr:all privileges to " + SlcConstants.ROLE_SLC);
+               }
+
+               JcrUtils.mkdirsSafe(session, RepoConstants.DEFAULT_ARTIFACTS_BASE_PATH);
+               Node proxiedRepositories = JcrUtils.mkdirsSafe(session, RepoConstants.PROXIED_REPOSITORIES);
+               for (RemoteRepository repository : defaultRepositories) {
+                       if (!proxiedRepositories.hasNode(repository.getId())) {
+                               Node proxiedRepository = proxiedRepositories.addNode(repository.getId());
+                               proxiedRepository.addMixin(NodeType.MIX_REFERENCEABLE);
+                               JcrUtils.urlToAddressProperties(proxiedRepository, repository.getUrl());
+                               // proxiedRepository.setProperty(SLC_URL, repository.getUrl());
+                               proxiedRepository.setProperty(SLC_TYPE, repository.getContentType());
+                       }
+               }
+       }
+
+       /**
+        * Retrieve and add this file to the repository
+        */
+       @Override
+       protected Node retrieve(Session session, String path) {
+               try {
+                       if (session.hasPendingChanges())
+                               throw new SlcException("Session has pending changed");
+                       Node node = null;
+                       for (Node proxiedRepository : getBaseUrls(session)) {
+                               String baseUrl = JcrUtils.urlFromAddressProperties(proxiedRepository);
+                               node = proxyUrl(session, baseUrl, path);
+                               if (node != null) {
+                                       node.addMixin(SlcTypes.SLC_KNOWN_ORIGIN);
+                                       Node origin = node.addNode(SLC_ORIGIN, SlcTypes.SLC_PROXIED);
+                                       origin.setProperty(SLC_PROXY, proxiedRepository);
+                                       JcrUtils.urlToAddressProperties(origin, baseUrl + path);
+                                       if (log.isDebugEnabled())
+                                               log.debug("Imported " + baseUrl + path + " to " + node);
+                                       return node;
+                               }
+                       }
+                       if (log.isDebugEnabled())
+                               log.warn("No proxy found for " + path);
+                       return null;
+               } catch (Exception e) {
+                       throw new SlcException("Cannot proxy " + path, e);
+               }
+       }
+
+       protected synchronized List<Node> getBaseUrls(Session session) throws RepositoryException {
+               List<Node> baseUrls = new ArrayList<Node>();
+               for (NodeIterator nit = session.getNode(RepoConstants.PROXIED_REPOSITORIES).getNodes(); nit.hasNext();) {
+                       Node proxiedRepository = nit.nextNode();
+                       baseUrls.add(proxiedRepository);
+               }
+               return baseUrls;
+       }
+
+       public void setDefaultRepositories(List<RemoteRepository> defaultRepositories) {
+               this.defaultRepositories = defaultRepositories;
+       }
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/maven/Migration_01_03.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/maven/Migration_01_03.java
new file mode 100644 (file)
index 0000000..8e20125
--- /dev/null
@@ -0,0 +1,358 @@
+package org.argeo.slc.repo.maven;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.jar.Attributes.Name;
+import java.util.jar.Manifest;
+
+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.nodetype.NodeType;
+import javax.jcr.query.QueryManager;
+import javax.jcr.query.QueryResult;
+import javax.jcr.query.qom.Ordering;
+import javax.jcr.query.qom.QueryObjectModel;
+import javax.jcr.query.qom.QueryObjectModelFactory;
+import javax.jcr.query.qom.Selector;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.NameVersion;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.argeo.slc.repo.ArtifactIndexer;
+import org.argeo.slc.repo.JarFileIndexer;
+import org.argeo.slc.repo.RepoUtils;
+import org.argeo.slc.repo.osgi.OsgiProfile;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.osgi.framework.Constants;
+
+/**
+ * Migrate the distribution from 1.2 to 1.4 by cleaning naming and dependencies.
+ * The dependency to the SpringSource Enterprise Bundle repository is removed as
+ * well as their naming conventions. All third party are move to org.argeo.tp
+ * group IDs. Maven dependency for Eclipse artifacts don't use version ranges
+ * anymore. Verison constraints on javax.* packages are removed (since they lead
+ * to "use package conflicts" when Eclipse and Spring Security are used
+ * together).
+ */
+public class Migration_01_03 implements Runnable, SlcNames {
+       final String SPRING_SOURCE_PREFIX = "com.springsource";
+       private final static CmsLog log = CmsLog.getLog(Migration_01_03.class);
+
+       private Repository repository;
+       private String sourceWorkspace;
+       private String targetWorkspace;
+
+       private List<String> excludedBundles = new ArrayList<String>();
+       private Map<String, String> symbolicNamesMapping = new HashMap<String, String>();
+
+       private Session origSession;
+       private Session targetSession;
+
+       private List<String> systemPackages = OsgiProfile.PROFILE_JAVA_SE_1_6.getSystemPackages();
+
+       private String artifactBasePath = "/";
+
+       private ArtifactIndexer artifactIndexer = new ArtifactIndexer();
+       private JarFileIndexer jarFileIndexer = new JarFileIndexer();
+
+       public void init() throws RepositoryException {
+               origSession = JcrUtils.loginOrCreateWorkspace(repository, sourceWorkspace);
+               targetSession = JcrUtils.loginOrCreateWorkspace(repository, targetWorkspace);
+
+               // works only in OSGi!!
+               // systemPackages = Arrays.asList(System.getProperty(
+               // "org.osgi.framework.system.packages").split(","));
+       }
+
+       public void destroy() {
+               JcrUtils.logoutQuietly(origSession);
+               JcrUtils.logoutQuietly(targetSession);
+       }
+
+       public void run() {
+
+               try {
+                       // clear target
+                       NodeIterator nit = targetSession.getNode(artifactBasePath).getNodes();
+                       while (nit.hasNext()) {
+                               Node node = nit.nextNode();
+                               if (node.isNodeType(NodeType.NT_FOLDER) || node.isNodeType(NodeType.NT_UNSTRUCTURED)) {
+                                       node.remove();
+                                       node.getSession().save();
+                                       if (log.isDebugEnabled())
+                                               log.debug("Cleared " + node);
+                               }
+                       }
+
+                       NodeIterator origArtifacts = listArtifactVersions(origSession);
+                       // process
+                       while (origArtifacts.hasNext()) {
+                               Node origArtifactNode = origArtifacts.nextNode();
+                               if (log.isTraceEnabled())
+                                       log.trace(origArtifactNode);
+
+                               processOrigArtifactVersion(origArtifactNode);
+                       }
+               } catch (Exception e) {
+                       throw new SlcException("Cannot perform v1.3 migration from " + sourceWorkspace + " to " + targetWorkspace,
+                                       e);
+               } finally {
+                       JcrUtils.discardQuietly(targetSession);
+               }
+       }
+
+       protected void processOrigArtifactVersion(Node origArtifactNode) throws RepositoryException, IOException {
+               Artifact origArtifact = RepoUtils.asArtifact(origArtifactNode);
+
+               // skip eclipse artifacts
+               if ((origArtifact.getGroupId().startsWith("org.eclipse")
+                               && !(origArtifact.getArtifactId().equals("org.eclipse.osgi")
+                                               || origArtifact.getArtifactId().equals("org.eclipse.osgi.source")
+                                               || origArtifact.getArtifactId().startsWith("org.eclipse.rwt.widgets.upload")))
+                               || origArtifact.getArtifactId().startsWith("com.ibm.icu")) {
+                       if (log.isDebugEnabled())
+                               log.debug("Skip " + origArtifact);
+                       return;
+               }
+
+               // skip SpringSource ActiveMQ
+               if (origArtifact.getArtifactId().startsWith("com.springsource.org.apache.activemq"))
+                       return;
+
+               String origJarNodeName = MavenConventionsUtils.artifactFileName(origArtifact);
+               if (!origArtifactNode.hasNode(origJarNodeName))
+                       throw new SlcException("Cannot find jar node for " + origArtifactNode);
+               Node origJarNode = origArtifactNode.getNode(origJarNodeName);
+
+               // read MANIFEST
+               Binary manifestBinary = origJarNode.getProperty(SLC_MANIFEST).getBinary();
+               Manifest origManifest = new Manifest(manifestBinary.getStream());
+               JcrUtils.closeQuietly(manifestBinary);
+
+               Boolean manifestModified = false;
+               Manifest targetManifest = new Manifest(origManifest);
+
+               // transform symbolic name
+               String origSymbolicName = origManifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
+               final String targetSymbolicName;
+               if (symbolicNamesMapping.containsKey(origSymbolicName)) {
+                       targetSymbolicName = symbolicNamesMapping.get(origSymbolicName);
+               } else if (origSymbolicName.startsWith(SPRING_SOURCE_PREFIX)
+                               && !origSymbolicName.equals(SPRING_SOURCE_PREFIX + ".json")) {
+                       targetSymbolicName = origSymbolicName.substring(SPRING_SOURCE_PREFIX.length() + 1);
+               } else {
+                       targetSymbolicName = origSymbolicName;
+               }
+
+               if (!targetSymbolicName.equals(origSymbolicName)) {
+                       targetManifest.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, targetSymbolicName);
+                       manifestModified = true;
+                       if (log.isDebugEnabled())
+                               log.debug(
+                                               Constants.BUNDLE_SYMBOLICNAME + " to " + targetSymbolicName + " \t\tfrom " + origSymbolicName);
+               }
+
+               // skip excluded bundles
+               if (excludedBundles.contains(targetSymbolicName))
+                       return;
+
+               // check fragment host
+               if (origManifest.getMainAttributes().containsKey(new Name(Constants.FRAGMENT_HOST))) {
+                       String origFragmentHost = origManifest.getMainAttributes().getValue(Constants.FRAGMENT_HOST);
+                       String targetFragmentHost;
+                       if (symbolicNamesMapping.containsKey(origFragmentHost)) {
+                               targetFragmentHost = symbolicNamesMapping.get(origFragmentHost);
+                       } else if (origFragmentHost.startsWith(SPRING_SOURCE_PREFIX)
+                                       && !origFragmentHost.equals(SPRING_SOURCE_PREFIX + ".json")) {
+                               targetFragmentHost = origFragmentHost.substring(SPRING_SOURCE_PREFIX.length() + 1);
+                       } else if (origFragmentHost.equals("org.argeo.dep.jacob;bundle-version=\"[1.14.3,1.14.4)\"")) {
+                               // this one for those who think I cannot be pragmatic - mbaudier
+                               targetFragmentHost = "com.jacob;bundle-version=\"[1.14.3,1.14.4)\"";
+                       } else {
+                               targetFragmentHost = origFragmentHost;
+                       }
+
+                       if (!targetFragmentHost.equals(origFragmentHost)) {
+                               targetManifest.getMainAttributes().putValue(Constants.FRAGMENT_HOST, targetFragmentHost);
+                               manifestModified = true;
+                               if (log.isDebugEnabled())
+                                       log.debug(Constants.FRAGMENT_HOST + " to " + targetFragmentHost + " from " + origFragmentHost);
+                       }
+               }
+
+               // we assume there is no Require-Bundle in com.springsource.* bundles
+
+               // javax with versions
+               StringBuffer targetImportPackages = new StringBuffer("");
+               NodeIterator origImportPackages = origJarNode.getNodes(SLC_ + Constants.IMPORT_PACKAGE);
+               Boolean importPackagesModified = false;
+               while (origImportPackages.hasNext()) {
+                       Node importPackage = origImportPackages.nextNode();
+                       String pkg = importPackage.getProperty(SLC_NAME).getString();
+                       targetImportPackages.append(pkg);
+                       if (importPackage.hasProperty(SLC_VERSION)) {
+                               String sourceVersion = importPackage.getProperty(SLC_VERSION).getString();
+                               String targetVersion = sourceVersion;
+                               if (systemPackages.contains(pkg)) {
+                                       if (!(sourceVersion.trim().equals("0") || sourceVersion.trim().equals("0.0.0"))) {
+                                               targetVersion = null;
+                                               importPackagesModified = true;
+                                               if (log.isDebugEnabled())
+                                                       log.debug(origSymbolicName + ": Nullify version of " + pkg + " from " + sourceVersion);
+                                       }
+                               }
+                               if (targetVersion != null)
+                                       targetImportPackages.append(";version=\"").append(targetVersion).append("\"");
+                       }
+                       if (importPackage.hasProperty(SLC_OPTIONAL)) {
+                               Boolean optional = importPackage.getProperty(SLC_OPTIONAL).getBoolean();
+                               if (optional)
+                                       targetImportPackages.append(";resolution:=\"optional\"");
+
+                       }
+                       if (origImportPackages.hasNext())
+                               targetImportPackages.append(",");
+               }
+
+               if (importPackagesModified) {
+                       targetManifest.getMainAttributes().putValue(Constants.IMPORT_PACKAGE, targetImportPackages.toString());
+                       manifestModified = true;
+               }
+
+               if (!manifestModified && log.isTraceEnabled()) {
+                       log.trace("MANIFEST of " + origSymbolicName + " was not modified");
+               }
+
+               // target coordinates
+               final String targetGroupId;
+               if (origArtifact.getArtifactId().startsWith("org.eclipse.rwt.widgets.upload"))
+                       targetGroupId = "org.argeo.tp.rap";
+               else if (origArtifact.getArtifactId().startsWith("org.polymap"))
+                       targetGroupId = "org.argeo.tp.rap";
+               else if (origArtifact.getGroupId().startsWith("org.eclipse")
+                               && !origArtifact.getArtifactId().equals("org.eclipse.osgi"))
+                       throw new SlcException(origArtifact + " should have been excluded");// targetGroupId
+                                                                                                                                                               // =
+                                                                                                                                                               // "org.argeo.tp.eclipse";
+               else
+                       targetGroupId = "org.argeo.tp";
+
+               String targetArtifactId = targetSymbolicName.split(";")[0];
+               Artifact targetArtifact = new DefaultArtifact(targetGroupId, targetArtifactId, "jar",
+                               origArtifact.getVersion());
+               String targetParentPath = MavenConventionsUtils.artifactParentPath(artifactBasePath, targetArtifact);
+               String targetFileName = MavenConventionsUtils.artifactFileName(targetArtifact);
+               String targetJarPath = targetParentPath + '/' + targetFileName;
+
+               // copy
+               Node targetParentNode = JcrUtils.mkfolders(targetSession, targetParentPath);
+               targetSession.save();
+               if (manifestModified) {
+                       Binary origBinary = origJarNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary();
+                       byte[] targetJarBytes = RepoUtils.modifyManifest(origBinary.getStream(), targetManifest);
+                       JcrUtils.copyBytesAsFile(targetParentNode, targetFileName, targetJarBytes);
+                       JcrUtils.closeQuietly(origBinary);
+               } else {// just copy
+                       targetSession.getWorkspace().copy(sourceWorkspace, origJarNode.getPath(), targetJarPath);
+               }
+               targetSession.save();
+
+               // reindex
+               Node targetJarNode = targetSession.getNode(targetJarPath);
+               artifactIndexer.index(targetJarNode);
+               jarFileIndexer.index(targetJarNode);
+
+               targetSession.save();
+
+               // sources
+               Artifact origSourceArtifact = new DefaultArtifact(origArtifact.getGroupId(),
+                               origArtifact.getArtifactId() + ".source", "jar", origArtifact.getVersion());
+               String origSourcePath = MavenConventionsUtils.artifactPath(artifactBasePath, origSourceArtifact);
+               if (origSession.itemExists(origSourcePath)) {
+                       Node origSourceJarNode = origSession.getNode(origSourcePath);
+
+                       Artifact targetSourceArtifact = new DefaultArtifact(targetGroupId, targetArtifactId + ".source", "jar",
+                                       origArtifact.getVersion());
+                       String targetSourceParentPath = MavenConventionsUtils.artifactParentPath(artifactBasePath,
+                                       targetSourceArtifact);
+                       String targetSourceFileName = MavenConventionsUtils.artifactFileName(targetSourceArtifact);
+                       String targetSourceJarPath = targetSourceParentPath + '/' + targetSourceFileName;
+
+                       Node targetSourceParentNode = JcrUtils.mkfolders(targetSession, targetSourceParentPath);
+                       targetSession.save();
+
+                       if (!targetSymbolicName.equals(origSymbolicName)) {
+                               Binary origBinary = origSourceJarNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA)
+                                               .getBinary();
+                               NameVersion targetNameVersion = RepoUtils.readNameVersion(targetManifest);
+                               byte[] targetJarBytes = RepoUtils.packageAsPdeSource(origBinary.getStream(), targetNameVersion);
+                               JcrUtils.copyBytesAsFile(targetSourceParentNode, targetSourceFileName, targetJarBytes);
+                               JcrUtils.closeQuietly(origBinary);
+                       } else {// just copy
+                               targetSession.getWorkspace().copy(sourceWorkspace, origSourceJarNode.getPath(), targetSourceJarPath);
+                       }
+                       targetSession.save();
+
+                       // reindex
+                       Node targetSourceJarNode = targetSession.getNode(targetSourceJarPath);
+                       artifactIndexer.index(targetSourceJarNode);
+                       jarFileIndexer.index(targetSourceJarNode);
+
+                       targetSession.save();
+               }
+       }
+
+       /*
+        * UTILITIES
+        */
+
+       static NodeIterator listArtifactVersions(Session session) throws RepositoryException {
+               QueryManager queryManager = session.getWorkspace().getQueryManager();
+               QueryObjectModelFactory factory = queryManager.getQOMFactory();
+
+               final String artifactVersionsSelector = "artifactVersions";
+               Selector source = factory.selector(SlcTypes.SLC_ARTIFACT_VERSION_BASE, artifactVersionsSelector);
+
+               Ordering orderByArtifactId = factory
+                               .ascending(factory.propertyValue(artifactVersionsSelector, SlcNames.SLC_ARTIFACT_ID));
+               Ordering[] orderings = { orderByArtifactId };
+
+               QueryObjectModel query = factory.createQuery(source, null, orderings, null);
+
+               QueryResult result = query.execute();
+               return result.getNodes();
+       }
+
+       public void setRepository(Repository repository) {
+               this.repository = repository;
+       }
+
+       public void setSourceWorkspace(String sourceWorkspace) {
+               this.sourceWorkspace = sourceWorkspace;
+       }
+
+       public void setTargetWorkspace(String targetWorkspace) {
+               this.targetWorkspace = targetWorkspace;
+       }
+
+       public void setExcludedBundles(List<String> excludedBundles) {
+               this.excludedBundles = excludedBundles;
+       }
+
+       public void setSymbolicNamesMapping(Map<String, String> symbolicNamesMapping) {
+               this.symbolicNamesMapping = symbolicNamesMapping;
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ArchiveSourcesProvider.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ArchiveSourcesProvider.java
new file mode 100644 (file)
index 0000000..9088e28
--- /dev/null
@@ -0,0 +1,89 @@
+package org.argeo.slc.repo.osgi;
+
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.jar.JarEntry;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.Session;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.repo.OsgiFactory;
+
+public class ArchiveSourcesProvider implements SourcesProvider {
+       private final static CmsLog log = CmsLog.getLog(ArchiveSourcesProvider.class);
+
+       private OsgiFactory osgiFactory;
+       private String uri;
+       private String base = "";
+
+       @Override
+       public void writeSources(List<String> packages, ZipOutputStream zout) {
+               Session distSession = null;
+               ZipInputStream zin = null;
+               try {
+                       distSession = osgiFactory.openDistSession();
+
+                       if (log.isDebugEnabled())
+                               log.debug("Wrapping " + uri);
+
+                       Node distNode = osgiFactory.getDist(distSession, uri);
+                       zin = new ZipInputStream(
+                                       distNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary().getStream());
+
+                       // prepare
+                       Set<String> directories = new TreeSet<String>();
+                       for (String pkg : packages)
+                               if (!pkg.equals("META-INF"))
+                                       directories.add(base + pkg.replace('.', '/') + '/');
+
+                       ZipEntry zentry = null;
+                       entries: while ((zentry = zin.getNextEntry()) != null) {
+                               String name = zentry.getName();
+                               if (!name.startsWith(base))
+                                       continue entries;
+
+                               String dirPath = FilenameUtils.getPath(name);
+                               if (name.equals(dirPath))// directory
+                                       continue entries;
+
+                               if (directories.contains(dirPath)) {
+                                       String path = name.substring(base.length());
+                                       zout.putNextEntry(new JarEntry(path));
+                                       IOUtils.copy(zin, zout);
+                                       zin.closeEntry();
+                                       zout.closeEntry();
+                                       continue entries;
+                               }
+                       }
+               } catch (Exception e) {
+                       throw new SlcException("Cannot retrieve sources from " + uri, e);
+               } finally {
+                       IOUtils.closeQuietly(zin);
+                       JcrUtils.logoutQuietly(distSession);
+               }
+
+       }
+
+       public void setOsgiFactory(OsgiFactory osgiFactory) {
+               this.osgiFactory = osgiFactory;
+       }
+
+       public void setUri(String uri) {
+               this.uri = uri;
+       }
+
+       public void setBase(String base) {
+               this.base = base;
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ArchiveWrapper.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ArchiveWrapper.java
new file mode 100644 (file)
index 0000000..3cb1e9c
--- /dev/null
@@ -0,0 +1,432 @@
+package org.argeo.slc.repo.osgi;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.jar.JarInputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.CategoryNameVersion;
+import org.argeo.slc.DefaultNameVersion;
+import org.argeo.slc.ModuleSet;
+import org.argeo.slc.NameVersion;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.build.Distribution;
+import org.argeo.slc.build.License;
+import org.argeo.slc.repo.OsgiFactory;
+import org.argeo.slc.repo.RepoUtils;
+import org.argeo.slc.repo.internal.springutil.AntPathMatcher;
+import org.argeo.slc.repo.internal.springutil.PathMatcher;
+import org.argeo.slc.repo.maven.ArtifactIdComparator;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+
+import aQute.bnd.osgi.Jar;
+
+/**
+ * Download a software distribution and generates the related OSGi bundles from
+ * the jars, or import them directly if they are already OSGi bundles and don't
+ * need further modification.
+ */
+public class ArchiveWrapper implements Runnable, ModuleSet, Distribution {
+       private final static CmsLog log = CmsLog.getLog(ArchiveWrapper.class);
+
+       private OsgiFactory osgiFactory;
+       private String version;
+       private License license;
+
+       private String uri;
+
+       /** Jars to wrap as OSGi bundles */
+       private Map<String, BndWrapper> wrappers = new HashMap<String, BndWrapper>();
+
+       private SourcesProvider sourcesProvider;
+
+       // pattern of OSGi bundles to import
+       private PathMatcher pathMatcher = new AntPathMatcher();
+       private Map<String, String> includes = new HashMap<String, String>();
+       private List<String> excludes = new ArrayList<String>();
+
+       private Boolean mavenGroupIndexes = false;
+
+       public void init() {
+               for (BndWrapper wrapper : wrappers.values()) {
+                       wrapper.setFactory(this);
+                       if (version != null && wrapper.getVersion() == null)
+                               wrapper.setVersion(version);
+                       if (license != null && wrapper.getLicense() == null)
+                               wrapper.setLicense(license);
+               }
+       }
+
+       public void destroy() {
+
+       }
+
+       public String getDistributionId() {
+               return uri;
+       }
+
+       public String getVersion() {
+               return version;
+       }
+
+       public License getLicense() {
+               return license;
+       }
+
+       public String getUri() {
+               return uri;
+       }
+
+       public Iterator<? extends NameVersion> nameVersions() {
+               if (wrappers.size() > 0)
+                       return wrappers.values().iterator();
+               else
+                       return osgiNameVersions();
+       }
+
+       @SuppressWarnings("resource")
+       protected Iterator<? extends NameVersion> osgiNameVersions() {
+               List<CategoryNameVersion> nvs = new ArrayList<CategoryNameVersion>();
+
+               Session distSession = null;
+               ZipInputStream zin = null;
+               try {
+                       distSession = osgiFactory.openDistSession();
+
+                       Node distNode = osgiFactory.getDist(distSession, uri);
+                       zin = new ZipInputStream(
+                                       distNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary().getStream());
+
+                       ZipEntry zentry = null;
+                       entries: while ((zentry = zin.getNextEntry()) != null) {
+                               String name = zentry.getName();
+                               if (log.isTraceEnabled())
+                                       log.trace("Zip entry " + name);
+                               for (String exclude : excludes)
+                                       if (pathMatcher.match(exclude, name))
+                                               continue entries;
+
+                               for (String include : includes.keySet()) {
+                                       if (pathMatcher.match(include, name)) {
+                                               String groupId = includes.get(include);
+                                               JarInputStream jis = new JarInputStream(zin);
+                                               if (jis.getManifest() == null) {
+                                                       log.warn("No MANIFEST in entry " + name + ", skipping...");
+                                                       continue entries;
+                                               }
+                                               NameVersion nv = RepoUtils.readNameVersion(jis.getManifest());
+                                               if (nv != null) {
+                                                       if (nv.getName().endsWith(".source"))
+                                                               continue entries;
+                                                       CategoryNameVersion cnv = new ArchiveWrapperCNV(groupId, nv.getName(), nv.getVersion(),
+                                                                       this);
+                                                       nvs.add(cnv);
+                                                       // no need to process further includes
+                                                       continue entries;
+                                               }
+                                       }
+                               }
+                       }
+                       return nvs.iterator();
+               } catch (Exception e) {
+                       throw new SlcException("Cannot wrap distribution " + uri, e);
+               } finally {
+                       IOUtils.closeQuietly(zin);
+                       JcrUtils.logoutQuietly(distSession);
+               }
+       }
+
+       public void run() {
+               if (mavenGroupIndexes && (version == null))
+                       throw new SlcException("'mavenGroupIndexes' requires 'version' to be set");
+
+               Map<String, Set<Artifact>> binaries = new HashMap<String, Set<Artifact>>();
+               Map<String, Set<Artifact>> sources = new HashMap<String, Set<Artifact>>();
+
+               Session distSession = null;
+               Session javaSession = null;
+               ZipInputStream zin = null;
+               try {
+                       javaSession = osgiFactory.openJavaSession();
+                       distSession = osgiFactory.openDistSession();
+
+                       if (log.isDebugEnabled())
+                               log.debug("Wrapping " + uri);
+                       boolean nothingWasDone = true;
+
+                       Node distNode = osgiFactory.getDist(distSession, uri);
+                       zin = new ZipInputStream(
+                                       distNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary().getStream());
+
+                       ZipEntry zentry = null;
+                       entries: while ((zentry = zin.getNextEntry()) != null) {
+                               String name = zentry.getName();
+
+                               // sources autodetect
+                               String baseName = FilenameUtils.getBaseName(name);
+                               if (baseName.endsWith("-sources")) {
+                                       String bundle = baseName.substring(0, baseName.length() - "-sources".length());
+                                       // log.debug(name + "," + baseName + ", " + bundle);
+                                       String bundlePath = FilenameUtils.getPath(name) + bundle + ".jar";
+                                       if (wrappers.containsKey(bundlePath)) {
+                                               BndWrapper wrapper = wrappers.get(bundlePath);
+                                               NameVersion bundleNv = new DefaultNameVersion(wrapper.getName(), wrapper.getVersion());
+                                               byte[] pdeSource = RepoUtils.packageAsPdeSource(zin, bundleNv);
+                                               Artifact sourcesArtifact = new DefaultArtifact(wrapper.getCategory(),
+                                                               wrapper.getName() + ".source", "jar", wrapper.getVersion());
+                                               Node pdeSourceNode = RepoUtils.copyBytesAsArtifact(javaSession.getRootNode(), sourcesArtifact,
+                                                               pdeSource);
+                                               osgiFactory.indexNode(pdeSourceNode);
+                                               pdeSourceNode.getSession().save();
+                                               if (log.isDebugEnabled())
+                                                       log.debug("Added sources " + sourcesArtifact + " for bundle " + wrapper.getArtifact()
+                                                                       + "from " + name + " in binary archive.");
+                                       }
+
+                               }
+                               // else if (baseName.endsWith(".source")) {
+                               // }
+
+                               // binaries
+                               if (wrappers.containsKey(name)) {
+                                       BndWrapper wrapper = (BndWrapper) wrappers.get(name);
+                                       // we must copy since the stream is closed by BND
+                                       byte[] origJarBytes = IOUtils.toByteArray(zin);
+                                       Artifact artifact = wrapZipEntry(javaSession, zentry, origJarBytes, wrapper);
+                                       nothingWasDone = false;
+                                       addArtifactToIndex(binaries, wrapper.getGroupId(), artifact);
+                               } else {
+                                       for (String wrapperKey : wrappers.keySet())
+                                               if (pathMatcher.match(wrapperKey, name)) {
+                                                       // first matched is taken
+                                                       BndWrapper wrapper = (BndWrapper) wrappers.get(wrapperKey);
+                                                       // we must copy since the stream is closed by BND
+                                                       byte[] origJarBytes = IOUtils.toByteArray(zin);
+                                                       Artifact artifact = wrapZipEntry(javaSession, zentry, origJarBytes, wrapper);
+                                                       nothingWasDone = false;
+                                                       addArtifactToIndex(binaries, wrapper.getGroupId(), artifact);
+                                                       continue entries;
+                                               } else {
+                                                       if (log.isTraceEnabled())
+                                                               log.trace(name + " not matched by " + wrapperKey);
+                                               }
+
+                                       for (String exclude : excludes)
+                                               if (pathMatcher.match(exclude, name))
+                                                       continue entries;
+
+                                       for (String include : includes.keySet()) {
+                                               if (pathMatcher.match(include, name)) {
+                                                       String groupId = includes.get(include);
+                                                       byte[] origJarBytes = IOUtils.toByteArray(zin);
+                                                       Artifact artifact = importZipEntry(javaSession, zentry, origJarBytes, groupId);
+                                                       if (artifact == null) {
+                                                               log.warn("Skipped non identified " + zentry);
+                                                               continue entries;
+                                                       }
+                                                       nothingWasDone = false;
+                                                       if (artifact.getArtifactId().endsWith(".source"))
+                                                               addArtifactToIndex(sources, groupId, artifact);
+                                                       else
+                                                               addArtifactToIndex(binaries, groupId, artifact);
+                                                       // no need to process this entry further
+                                                       continue entries;
+                                               }
+                                       }
+                               }
+                       }
+
+                       // indexes
+                       if (mavenGroupIndexes && version != null) {
+                               for (String groupId : binaries.keySet()) {
+                                       RepoUtils.writeGroupIndexes(javaSession, "/", groupId, version, binaries.get(groupId),
+                                                       sources.containsKey(groupId) ? sources.get(groupId) : null);
+                               }
+                       }
+
+                       if (nothingWasDone) {
+                               log.error("Nothing was done when wrapping " + uri + ". THE DISTRIBUTION IS INCONSISTENT.");
+                               // throw new SlcException("Nothing was done");
+                               // TODO Fail if not all wrappers matched
+                       }
+
+               } catch (Exception e) {
+                       throw new SlcException("Cannot wrap distribution " + uri, e);
+               } finally {
+                       IOUtils.closeQuietly(zin);
+                       JcrUtils.logoutQuietly(distSession);
+                       JcrUtils.logoutQuietly(javaSession);
+               }
+       }
+
+       protected Artifact wrapZipEntry(Session javaSession, ZipEntry zentry, byte[] origJarBytes, BndWrapper wrapper)
+                       throws RepositoryException {
+               ByteArrayOutputStream out = null;
+               ByteArrayInputStream in = null;
+               Node newJarNode;
+               Jar jar = null;
+               try {
+                       out = new ByteArrayOutputStream((int) zentry.getSize());
+                       in = new ByteArrayInputStream(origJarBytes);
+                       wrapper.wrapJar(in, out);
+
+                       Artifact artifact = wrapper.getArtifact();
+                       newJarNode = RepoUtils.copyBytesAsArtifact(javaSession.getRootNode(), artifact, out.toByteArray());
+                       osgiFactory.indexNode(newJarNode);
+                       newJarNode.getSession().save();
+                       if (log.isDebugEnabled())
+                               log.debug("Wrapped jar " + zentry.getName() + " to " + newJarNode.getPath());
+
+                       if (sourcesProvider != null)
+                               addSource(javaSession, artifact, out.toByteArray());
+
+                       return artifact;
+               } finally {
+                       IOUtils.closeQuietly(in);
+                       IOUtils.closeQuietly(out);
+                       if (jar != null)
+                               jar.close();
+               }
+       }
+
+       protected void addSource(Session javaSession, Artifact artifact, byte[] binaryJarBytes) {
+               InputStream in = null;
+               ByteArrayOutputStream out = null;
+               Jar jar = null;
+               try {
+                       in = new ByteArrayInputStream(binaryJarBytes);
+                       jar = new Jar(null, in);
+                       List<String> packages = jar.getPackages();
+
+                       out = new ByteArrayOutputStream();
+                       sourcesProvider.writeSources(packages, new ZipOutputStream(out));
+
+                       IOUtils.closeQuietly(in);
+                       in = new ByteArrayInputStream(out.toByteArray());
+                       byte[] sourcesJar = RepoUtils.packageAsPdeSource(in,
+                                       new DefaultNameVersion(artifact.getArtifactId(), artifact.getVersion()));
+                       Artifact sourcesArtifact = new DefaultArtifact(artifact.getGroupId(), artifact.getArtifactId() + ".source",
+                                       "jar", artifact.getVersion());
+                       Node sourcesJarNode = RepoUtils.copyBytesAsArtifact(javaSession.getRootNode(), sourcesArtifact, sourcesJar);
+                       sourcesJarNode.getSession().save();
+
+                       if (log.isDebugEnabled())
+                               log.debug("Added sources " + sourcesArtifact + " for bundle " + artifact + "from source provider "
+                                               + sourcesProvider);
+               } catch (Exception e) {
+                       throw new SlcException("Cannot get sources for " + artifact, e);
+               } finally {
+                       IOUtils.closeQuietly(in);
+                       IOUtils.closeQuietly(out);
+                       if (jar != null)
+                               jar.close();
+               }
+       }
+
+       protected Artifact importZipEntry(Session javaSession, ZipEntry zentry, byte[] binaryJarBytes, String groupId)
+                       throws RepositoryException {
+               ByteArrayInputStream in = null;
+               Node newJarNode;
+               try {
+                       in = new ByteArrayInputStream(binaryJarBytes);
+                       NameVersion nameVersion = RepoUtils.readNameVersion(in);
+                       if (nameVersion == null) {
+                               log.warn("Cannot identify " + zentry.getName());
+                               return null;
+                       }
+                       Artifact artifact = new DefaultArtifact(groupId, nameVersion.getName(), "jar", nameVersion.getVersion());
+                       newJarNode = RepoUtils.copyBytesAsArtifact(javaSession.getRootNode(), artifact, binaryJarBytes);
+                       osgiFactory.indexNode(newJarNode);
+                       newJarNode.getSession().save();
+                       if (log.isDebugEnabled()) {
+                               log.debug(zentry.getName() + " => " + artifact);
+                       }
+
+                       if (sourcesProvider != null)
+                               addSource(javaSession, artifact, binaryJarBytes);
+
+                       return artifact;
+               } finally {
+                       IOUtils.closeQuietly(in);
+               }
+       }
+
+       private void addArtifactToIndex(Map<String, Set<Artifact>> index, String groupId, Artifact artifact) {
+               if (!index.containsKey(groupId))
+                       index.put(groupId, new TreeSet<Artifact>(new ArtifactIdComparator()));
+               index.get(groupId).add(artifact);
+       }
+
+       public void setUri(String uri) {
+               this.uri = uri;
+       }
+
+       public void setWrappers(Map<String, BndWrapper> wrappers) {
+               this.wrappers = wrappers;
+       }
+
+       public void setOsgiFactory(OsgiFactory osgiFactory) {
+               this.osgiFactory = osgiFactory;
+       }
+
+       public void setVersion(String version) {
+               this.version = version;
+       }
+
+       public void setLicense(License license) {
+               this.license = license;
+       }
+
+       public void setPathMatcher(PathMatcher pathMatcher) {
+               this.pathMatcher = pathMatcher;
+       }
+
+       public void setIncludes(Map<String, String> includes) {
+               this.includes = includes;
+       }
+
+       public void setExcludes(List<String> excludes) {
+               this.excludes = excludes;
+       }
+
+       public void setMavenGroupIndexes(Boolean mavenGroupIndexes) {
+               this.mavenGroupIndexes = mavenGroupIndexes;
+       }
+
+       public void setSourcesProvider(SourcesProvider sourcesProvider) {
+               this.sourcesProvider = sourcesProvider;
+       }
+
+       public Map<String, BndWrapper> getWrappers() {
+               return wrappers;
+       }
+
+       public Map<String, String> getIncludes() {
+               return includes;
+       }
+
+       public List<String> getExcludes() {
+               return excludes;
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ArchiveWrapperCNV.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ArchiveWrapperCNV.java
new file mode 100644 (file)
index 0000000..910d581
--- /dev/null
@@ -0,0 +1,25 @@
+package org.argeo.slc.repo.osgi;
+
+import org.argeo.slc.DefaultCategoryNameVersion;
+
+/** A module within an archive. */
+public class ArchiveWrapperCNV extends DefaultCategoryNameVersion implements Runnable {
+       /** Build runnable */
+       private ArchiveWrapper build;
+
+       public ArchiveWrapperCNV(String category, String name, String version, ArchiveWrapper build) {
+               super(category, name, version);
+               this.build = build;
+       }
+
+       @Override
+       public void run() {
+               if (build != null)
+                       build.run();
+       }
+
+       public ArchiveWrapper getBuild() {
+               return build;
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ArgeoOsgiDistributionImpl.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ArgeoOsgiDistributionImpl.java
new file mode 100644 (file)
index 0000000..dbfd576
--- /dev/null
@@ -0,0 +1,281 @@
+package org.argeo.slc.repo.osgi;
+
+import static org.argeo.slc.ManifestConstants.SLC_ORIGIN_M2;
+import static org.argeo.slc.ManifestConstants.SLC_ORIGIN_URI;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+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.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.slc.CategoryNameVersion;
+import org.argeo.slc.ManifestConstants;
+import org.argeo.slc.ModuleSet;
+import org.argeo.slc.NameVersion;
+import org.argeo.slc.build.Distribution;
+import org.argeo.slc.execution.ExecutionFlow;
+import org.argeo.slc.repo.ArgeoOsgiDistribution;
+import org.argeo.slc.repo.ArtifactDistribution;
+import org.argeo.slc.repo.FreeLicense;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.osgi.framework.Constants;
+
+/**
+ * A consistent and versioned OSGi distribution, which can be built and tested.
+ */
+public class ArgeoOsgiDistributionImpl extends ArtifactDistribution implements ArgeoOsgiDistribution {
+       private final static CmsLog log = CmsLog.getLog(ArgeoOsgiDistributionImpl.class);
+
+       private List<Object> modules = new ArrayList<Object>();
+
+       public ArgeoOsgiDistributionImpl(String coords) {
+               super(coords);
+       }
+
+       public void init() {
+               if (log.isDebugEnabled())
+                       log.debug(describe());
+               migrateTov2(Paths.get(System.getProperty("user.home"), "dev/git/unstable/argeo-tp/migration"));
+       }
+
+       public void destroy() {
+
+       }
+
+       public String describe() {
+               SortedSet<String> sort = new TreeSet<String>();
+               Iterator<? extends NameVersion> nvIt = nameVersions();
+               while (nvIt.hasNext()) {
+                       NameVersion nv = nvIt.next();
+                       String str = nv.toString();
+                       if (nv instanceof MavenWrapper)
+                               str = str + "\t(Maven)";
+                       else if (nv instanceof UriWrapper)
+                               str = str + "\t(URI)";
+                       else if (nv instanceof ArchiveWrapperCNV)
+                               str = str + "\t(OSGi from archive)";
+                       else if (nv instanceof BndWrapper)
+                               str = str + "\t(Plain BND from archive)";
+                       else
+                               str = str + "\t(UNKNOWN??)";
+                       sort.add(str);
+               }
+
+               StringBuffer buf = new StringBuffer("## DISTRIBUTION " + toString() + " ##\n");
+               for (String str : sort) {
+                       buf.append(str).append('\n');
+               }
+               return buf.toString();
+       }
+
+       public void migrateTov2(Path baseDir) {
+               Set<ArchiveWrapper> archiveWrappers = new HashSet<>();
+               Iterator<? extends NameVersion> nvIt = nameVersions();
+               while (nvIt.hasNext()) {
+                       NameVersion nv = nvIt.next();
+                       try {
+                               if (nv instanceof CategoryNameVersion) {
+                                       CategoryNameVersion cnv = (CategoryNameVersion) nv;
+                                       // TODO add branch?
+                                       Path categoryBase = baseDir.resolve(cnv.getCategory());
+                                       Files.createDirectories(categoryBase);
+                                       if (cnv instanceof BndWrapper) {
+                                               BndWrapper bw = (BndWrapper) cnv;
+                                               Path bndPath = categoryBase.resolve(cnv.getName() + ".bnd");
+                                               Map<String, String> props = new TreeMap<>();
+                                               for (Map.Entry<Object, Object> entry : ((BndWrapper) cnv).getBndProperties().entrySet()) {
+                                                       props.put(entry.getKey().toString(), entry.getValue().toString());
+                                               }
+                                               props.put(Constants.BUNDLE_SYMBOLICNAME, cnv.getName());
+                                               props.put(Constants.BUNDLE_VERSION, cnv.getVersion());
+                                               if (bw.getLicense() != null)
+                                                       props.put(Constants.BUNDLE_LICENSE, bw.getLicense().toString());
+                                               else
+                                                       log.warn("No license for " + cnv);
+                                               if (bw.getDoNotModify()) {
+                                                       props.put(ManifestConstants.SLC_ORIGIN_MANIFEST_NOT_MODIFIED.toString(), "true");
+                                               }
+                                               // props.put("SLC-Category", cnv.getCategory());
+
+                                               if (cnv instanceof MavenWrapper) {
+                                                       MavenWrapper mw = (MavenWrapper) cnv;
+                                                       String sourceCoords = mw.getSourceCoords();
+                                                       props.put(SLC_ORIGIN_M2.toString(), sourceCoords);
+                                                       Artifact mavenCnv = new DefaultArtifact(sourceCoords);
+                                                       if (mavenCnv.getArtifactId().equals(cnv.getName()))
+                                                               props.remove(Constants.BUNDLE_SYMBOLICNAME);
+                                                       if (mavenCnv.getVersion().equals(cnv.getVersion()))
+                                                               props.remove(Constants.BUNDLE_VERSION);
+                                               } else if (cnv instanceof UriWrapper) {
+                                                       UriWrapper mw = (UriWrapper) cnv;
+                                                       props.put(SLC_ORIGIN_URI.toString(), mw.getEffectiveUri());
+                                                       if (mw.getUri() == null && mw.getBaseUri() != null) {
+                                                               log.warn("Base URI for " + cnv);
+                                                               props.put("SLC-Origin-BaseURI", mw.getBaseUri());
+                                                               props.put("SLC-Origin-VersionSeparator", mw.getVersionSeparator());
+                                                       }
+                                               } else {
+                                                       log.warn("Unidentified BND wrapper " + cnv);
+                                               }
+
+                                               // write BND file
+                                               try (Writer writer = Files.newBufferedWriter(bndPath)) {
+                                                       // writer.write("# " + cnv + "\n");
+                                                       props: for (String key : props.keySet()) {
+                                                               String value = props.get(key);
+                                                               if (Constants.EXPORT_PACKAGE.equals(key) && "*".equals(value.trim()))
+                                                                       continue props;
+
+                                                               writer.write(key + ": " + value + '\n');
+                                                       }
+                                                       if (log.isTraceEnabled())
+                                                               log.trace("Wrote " + bndPath);
+                                               }
+                                       } else if (cnv instanceof ArchiveWrapperCNV) {
+                                               ArchiveWrapperCNV onv = (ArchiveWrapperCNV) cnv;
+                                               ArchiveWrapper aw = onv.getBuild();
+                                               archiveWrappers.add(aw);
+                                               // TODO specify and implement archive wrapper support
+                                       } else {
+                                               log.warn("Unsupported wrapper " + cnv.getClass() + " for " + cnv);
+                                       }
+
+                               } else {
+                                       log.error("Category required for " + nv + ", skipping...");
+                               }
+                       } catch (IOException e) {
+                               log.error("Could not process " + nv, e);
+                       }
+               }
+               if (log.isDebugEnabled()) {
+                       for (ArchiveWrapper aw : archiveWrappers) {
+                               log.debug("Archive wrapper " + aw.getUri() + ":");
+                               log.debug(" includes: " + aw.getIncludes());
+                               log.debug(" excludes: " + aw.getExcludes());
+                               log.debug(" beans   : " + aw.getWrappers());
+
+                               String uri = aw.getUri();
+                               String duName = null;
+                               String category = null;
+                               String oldCategory = null;
+                               if (uri.startsWith("http://www.eclipse.org/downloads/rt/rap/3.10/e4/rap-e4")) {
+                                       duName = "eclipse-rap";
+                                       category = "org.argeo.tp.eclipse.rap";
+                                       oldCategory = "org.argeo.tp.rap.e4";
+                               } else if (uri.startsWith("http://www.eclipse.org/downloads/equinox/")) {
+                                       duName = "eclipse-equinox";
+                                       category = "org.argeo.tp.eclipse.equinox";
+                                       oldCategory = "org.argeo.tp.equinox";
+                               } else if (uri.startsWith("http://www.eclipse.org/downloads/eclipse/downloads/drops4")) {
+                                       duName = "eclipse-rcp";
+                                       category = "org.argeo.tp.eclipse.rcp";
+                                       oldCategory = "org.argeo.tp.rcp.e4";
+                               }
+
+                               if (duName != null) {
+                                       try {
+                                               Path duDir = baseDir.resolve(category).resolve(duName);
+                                               Files.createDirectories(duDir);
+                                               Path bndPath = duDir.resolve("common.bnd");
+                                               Path includesPath = duDir.resolve("includes.properties");
+
+                                               Map<String, String> props = new TreeMap<>();
+                                               props.put(ManifestConstants.SLC_ORIGIN_URI.toString(), aw.getUri());
+                                               props.put(ManifestConstants.SLC_ORIGIN_MANIFEST_NOT_MODIFIED.toString(), "true");
+                                               props.put(Constants.BUNDLE_LICENSE, FreeLicense.EPL.toString());
+                                               // write BND file
+                                               try (Writer bndWriter = Files.newBufferedWriter(bndPath);
+                                                               Writer includesWriter = Files.newBufferedWriter(includesPath);) {
+                                                       // writer.write("# " + cnv + "\n");
+                                                       props: for (String key : props.keySet()) {
+                                                               String value = props.get(key);
+                                                               if (Constants.EXPORT_PACKAGE.equals(key) && "*".equals(value.trim()))
+                                                                       continue props;
+
+                                                               bndWriter.write(key + ": " + value + '\n');
+                                                       }
+
+                                                       for (String key : aw.getIncludes().keySet()) {
+                                                               String value = aw.getIncludes().get(key);
+                                                               if (value.equals(oldCategory))
+                                                                       value = category;
+                                                               includesWriter.write(key + "=" + value + '\n');
+                                                       }
+                                                       if (log.isTraceEnabled())
+                                                               log.trace("Wrote " + bndPath);
+                                               }
+                                       } catch (IOException e) {
+                                               log.error("Could not process " + aw, e);
+                                       }
+
+                               }
+                       }
+               }
+
+       }
+
+       public Iterator<NameVersion> nameVersions() {
+               List<NameVersion> nameVersions = new ArrayList<NameVersion>();
+               for (Object module : modules) {
+                       // extract runnable from execution flow
+                       if (module instanceof ExecutionFlow) {
+                               for (Iterator<Runnable> it = ((ExecutionFlow) module).runnables(); it.hasNext();) {
+                                       processModule(nameVersions, it.next());
+                               }
+                       } else {
+                               processModule(nameVersions, module);
+                       }
+               }
+               return nameVersions.iterator();
+       }
+
+       private void processModule(List<NameVersion> nameVersions, Object module) {
+               if (module instanceof ModuleSet)
+                       addNameVersions(nameVersions, (ModuleSet) module);
+               else if (module instanceof NameVersion) {
+                       NameVersion nv = (NameVersion) module;
+                       addNameVersion(nameVersions, nv);
+               } else
+                       log.warn("Ignored " + module);
+       }
+
+       private void addNameVersions(List<NameVersion> nameVersions, ModuleSet moduleSet) {
+               Iterator<? extends NameVersion> it = moduleSet.nameVersions();
+               while (it.hasNext()) {
+                       NameVersion nv = it.next();
+                       addNameVersion(nameVersions, nv);
+               }
+       }
+
+       protected void addNameVersion(List<NameVersion> nameVersions, NameVersion nv) {
+               if (!nameVersions.contains(nv)) {
+                       nameVersions.add(nv);
+               }
+       }
+
+       // Modular distribution interface methods. Not yet used.
+       public Distribution getModuleDistribution(String moduleName, String moduleVersion) {
+               throw new UnsupportedOperationException();
+       }
+
+       public Object getModulesDescriptor(String descriptorType) {
+               throw new UnsupportedOperationException();
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setModules(List<Object> modules) {
+               this.modules = modules;
+       }
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/BndWrapper.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/BndWrapper.java
new file mode 100644 (file)
index 0000000..57d7a81
--- /dev/null
@@ -0,0 +1,216 @@
+package org.argeo.slc.repo.osgi;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Properties;
+import java.util.jar.Manifest;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.slc.CategoryNameVersion;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.build.Distribution;
+import org.argeo.slc.build.License;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.osgi.framework.Version;
+
+import aQute.bnd.osgi.Builder;
+import aQute.bnd.osgi.Constants;
+import aQute.bnd.osgi.Jar;
+
+/** Utilities around the BND library, which manipulates OSGi metadata. */
+public class BndWrapper implements Constants, CategoryNameVersion, Distribution {
+       private final static CmsLog log = CmsLog.getLog(BndWrapper.class);
+
+       private String groupId;
+       private String name;
+       private Properties bndProperties = new Properties();
+
+       private String version;
+       private License license;
+
+       private Boolean doNotModify = false;
+
+       private Runnable factory = null;
+
+       public void wrapJar(InputStream in, OutputStream out) {
+               Builder b = new Builder();
+               Jar jar = null;
+               try {
+                       byte[] jarBytes = IOUtils.toByteArray(in);
+
+                       jar = new Jar(name, new ByteArrayInputStream(jarBytes));
+                       Manifest sourceManifest = jar.getManifest();
+
+                       Version versionToUse;
+                       if (sourceManifest != null) {
+                               // Symbolic name
+                               String sourceSymbolicName = sourceManifest.getMainAttributes().getValue(BUNDLE_SYMBOLICNAME);
+                               if (sourceSymbolicName != null && !sourceSymbolicName.equals(name))
+                                       log.info("The new symbolic name (" + name
+                                                       + ") is not consistant with the wrapped bundle symbolic name (" + sourceSymbolicName + ")");
+
+                               // Version
+                               String sourceVersion = sourceManifest.getMainAttributes().getValue(BUNDLE_VERSION);
+                               if (getVersion() == null && sourceVersion == null) {
+                                       throw new SlcException("A bundle version must be defined.");
+                               } else if (getVersion() == null && sourceVersion != null) {
+                                       versionToUse = new Version(sourceVersion);
+                                       version = sourceVersion; // set wrapper version
+                               } else if (getVersion() != null && sourceVersion == null) {
+                                       versionToUse = new Version(getVersion());
+                               } else {// both set
+                                       versionToUse = new Version(getVersion());
+                                       Version sv = new Version(sourceVersion);
+                                       if (versionToUse.getMajor() != sv.getMajor() || versionToUse.getMinor() != sv.getMinor()
+                                                       || versionToUse.getMicro() != sv.getMicro()) {
+                                               log.warn("The new version (" + versionToUse
+                                                               + ") is not consistant with the wrapped bundle version (" + sv + ")");
+                                       }
+                               }
+                       } else {
+                               versionToUse = new Version(getVersion());
+                       }
+
+                       if (doNotModify) {
+                               IOUtils.write(jarBytes, out);
+                               // jar.write(out);
+                       } else {
+
+                               Properties properties = new Properties();
+                               properties.putAll(bndProperties);
+                               properties.setProperty(BUNDLE_SYMBOLICNAME, name);
+                               properties.setProperty(BUNDLE_VERSION, versionToUse.toString());
+
+                               // License
+                               if (license != null) {
+                                       properties.setProperty(BUNDLE_LICENSE, license.toString());
+                                       // TODO add LICENSE.TXT
+                               } else {
+                                       log.warn("No license set for " + toString());
+                               }
+
+                               // b.addIncluded(jarFile);
+                               b.addClasspath(jar);
+
+                               if (log.isDebugEnabled())
+                                       log.debug(properties);
+                               b.setProperties(properties);
+
+                               Jar newJar = b.build();
+                               newJar.write(out);
+                               newJar.close();
+                       }
+               } catch (Exception e) {
+                       throw new SlcException("Cannot wrap jar", e);
+               } finally {
+                       try {
+                               b.close();
+                               if (jar != null)
+                                       jar.close();
+                       } catch (Exception e) {
+                               // silent
+                       }
+               }
+
+       }
+
+       public Runnable getFactory() {
+               return factory;
+       }
+
+       public void setFactory(Runnable factory) {
+               if (this.factory != null)
+                       throw new SlcException("Factory already set on " + name);
+               this.factory = factory;
+       }
+
+       public void setName(String bsn) {
+               this.name = bsn;
+       }
+
+       public String getName() {
+               return name;
+       }
+
+       public void setVersion(String version) {
+               if (this.version != null)
+                       throw new SlcException("Version already set on " + name + " (" + this.version + ")");
+               this.version = version;
+       }
+
+       public String getVersion() {
+               return version;
+       }
+
+       public License getLicense() {
+               return license;
+       }
+
+       public void setLicense(License license) {
+               if (this.license != null)
+                       throw new SlcException("License already set on " + name);
+               this.license = license;
+       }
+
+       public Properties getBndProperties() {
+               return bndProperties;
+       }
+
+       public void setBndProperties(Properties bndProperties) {
+               this.bndProperties = bndProperties;
+       }
+
+       public String getGroupId() {
+               return groupId;
+       }
+
+       public String getCategory() {
+               return getGroupId();
+       }
+
+       public void setGroupId(String groupId) {
+               this.groupId = groupId;
+       }
+
+       public String getDistributionId() {
+               return getCategory() + ":" + getName() + ":" + getVersion();
+       }
+
+       public Artifact getArtifact() {
+               return new DefaultArtifact(groupId, name, "jar", getVersion());
+       }
+
+       @Override
+       public String toString() {
+               return getDistributionId();
+       }
+
+       @Override
+       public int hashCode() {
+               if (name != null)
+                       return name.hashCode();
+               return super.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj instanceof CategoryNameVersion) {
+                       CategoryNameVersion cnv = (CategoryNameVersion) obj;
+                       return getCategory().equals(cnv.getCategory()) && getName().equals(cnv.getName())
+                                       && getVersion().equals(cnv.getVersion());
+               } else
+                       return false;
+       }
+
+       public void setDoNotModify(Boolean doNotModify) {
+               this.doNotModify = doNotModify;
+       }
+
+       public Boolean getDoNotModify() {
+               return doNotModify;
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ImportBundlesZip.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ImportBundlesZip.java
new file mode 100644 (file)
index 0000000..626fa72
--- /dev/null
@@ -0,0 +1,144 @@
+package org.argeo.slc.repo.osgi;
+
+import java.io.ByteArrayInputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.Session;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.NameVersion;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.repo.ArtifactIndexer;
+import org.argeo.slc.repo.JarFileIndexer;
+import org.argeo.slc.repo.RepoUtils;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+
+/**
+ * Import all bundles in a zip file (typically an Eclipse distribution) into the
+ * workspace.
+ * 
+ * @deprecated Use {@link ArchiveWrapper} instead.
+ */
+@Deprecated
+public class ImportBundlesZip implements Runnable {
+       private final static CmsLog log = CmsLog.getLog(ImportBundlesZip.class);
+       private Repository repository;
+       private String workspace;
+       private String groupId;
+       private String artifactBasePath = "/";
+
+       private ArtifactIndexer artifactIndexer = new ArtifactIndexer();
+       private JarFileIndexer jarFileIndexer = new JarFileIndexer();
+
+       private String zipFile;
+
+       private List<String> excludedBundles = new ArrayList<String>();
+
+       public void run() {
+               ZipInputStream zipIn = null;
+               JarInputStream jarIn = null;
+               Session session = null;
+               try {
+                       URL url = new URL(zipFile);
+                       session = repository.login(workspace);
+
+                       // clear
+                       // String groupPath = MavenConventionsUtils.groupPath(
+                       // artifactBasePath, groupId);
+                       // if (session.itemExists(groupPath)) {
+                       // session.getNode(groupPath).remove();
+                       // session.save();
+                       // if (log.isDebugEnabled())
+                       // log.debug("Cleared " + groupPath);
+                       // }
+
+                       zipIn = new ZipInputStream(url.openStream());
+                       ZipEntry zipEntry = null;
+                       entries: while ((zipEntry = zipIn.getNextEntry()) != null) {
+                               String entryName = zipEntry.getName();
+                               if (!entryName.endsWith(".jar") || entryName.contains("feature"))
+                                       continue entries;// skip
+                               byte[] jarBytes = IOUtils.toByteArray(zipIn);
+                               zipIn.closeEntry();
+                               jarIn = new JarInputStream(new ByteArrayInputStream(jarBytes));
+                               Manifest manifest = jarIn.getManifest();
+                               IOUtils.closeQuietly(jarIn);
+                               if (manifest == null) {
+                                       log.warn(entryName + " has no MANIFEST");
+                                       continue entries;
+                               }
+                               NameVersion nv;
+                               try {
+                                       nv = RepoUtils.readNameVersion(manifest);
+                               } catch (Exception e) {
+                                       log.warn("Cannot read name version from " + entryName, e);
+                                       continue entries;
+                               }
+
+                               String bundleName = RepoUtils.extractBundleNameFromSourceName(nv.getName());
+                               // skip excluded bundles and their sources
+                               if (excludedBundles.contains(bundleName))
+                                       continue entries;
+                               // for(String excludedBundle:excludedBundles){
+                               // if(bundleName.contains(excludedBundle))
+                               // continue entries;
+                               // }
+
+                               Artifact artifact = new DefaultArtifact(groupId, nv.getName(), "jar", nv.getVersion());
+                               Node artifactNode = RepoUtils.copyBytesAsArtifact(session.getNode(artifactBasePath), artifact,
+                                               jarBytes);
+                               jarBytes = null;// superstition, in order to free memory
+
+                               // indexes
+                               artifactIndexer.index(artifactNode);
+                               jarFileIndexer.index(artifactNode);
+                               session.save();
+                               if (log.isDebugEnabled())
+                                       log.debug("Imported " + entryName + " to " + artifactNode);
+                       }
+               } catch (Exception e) {
+                       throw new SlcException("Cannot import zip " + zipFile + " to " + workspace, e);
+               } finally {
+                       IOUtils.closeQuietly(zipIn);
+                       IOUtils.closeQuietly(jarIn);
+                       JcrUtils.logoutQuietly(session);
+               }
+
+       }
+
+       public void setRepository(Repository repository) {
+               this.repository = repository;
+       }
+
+       public void setWorkspace(String workspace) {
+               this.workspace = workspace;
+       }
+
+       public void setGroupId(String groupId) {
+               this.groupId = groupId;
+       }
+
+       public void setArtifactBasePath(String artifactBasePath) {
+               this.artifactBasePath = artifactBasePath;
+       }
+
+       public void setZipFile(String zipFile) {
+               this.zipFile = zipFile;
+       }
+
+       public void setExcludedBundles(List<String> excludedBundles) {
+               this.excludedBundles = excludedBundles;
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/JavaSE-1.6.profile b/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/JavaSE-1.6.profile
new file mode 100644 (file)
index 0000000..68e811f
--- /dev/null
@@ -0,0 +1,194 @@
+###############################################################################
+# Copyright (c) 2003, 2008 IBM Corporation and others.
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+# 
+# Contributors:
+#     IBM Corporation - initial API and implementation
+###############################################################################
+org.osgi.framework.system.packages = \
+ javax.accessibility,\
+ javax.activation,\
+ javax.activity,\
+ javax.annotation,\
+ javax.annotation.processing,\
+ javax.crypto,\
+ javax.crypto.interfaces,\
+ javax.crypto.spec,\
+ javax.imageio,\
+ javax.imageio.event,\
+ javax.imageio.metadata,\
+ javax.imageio.plugins.bmp,\
+ javax.imageio.plugins.jpeg,\
+ javax.imageio.spi,\
+ javax.imageio.stream,\
+ javax.jws,\
+ javax.jws.soap,\
+ javax.lang.model,\
+ javax.lang.model.element,\
+ javax.lang.model.type,\
+ javax.lang.model.util,\
+ javax.management,\
+ javax.management.loading,\
+ javax.management.modelmbean,\
+ javax.management.monitor,\
+ javax.management.openmbean,\
+ javax.management.relation,\
+ javax.management.remote,\
+ javax.management.remote.rmi,\
+ javax.management.timer,\
+ javax.naming,\
+ javax.naming.directory,\
+ javax.naming.event,\
+ javax.naming.ldap,\
+ javax.naming.spi,\
+ javax.net,\
+ javax.net.ssl,\
+ javax.print,\
+ javax.print.attribute,\
+ javax.print.attribute.standard,\
+ javax.print.event,\
+ javax.rmi,\
+ javax.rmi.CORBA,\
+ javax.rmi.ssl,\
+ javax.script,\
+ javax.security.auth,\
+ javax.security.auth.callback,\
+ javax.security.auth.kerberos,\
+ javax.security.auth.login,\
+ javax.security.auth.spi,\
+ javax.security.auth.x500,\
+ javax.security.cert,\
+ javax.security.sasl,\
+ javax.sound.midi,\
+ javax.sound.midi.spi,\
+ javax.sound.sampled,\
+ javax.sound.sampled.spi,\
+ javax.sql,\
+ javax.sql.rowset,\
+ javax.sql.rowset.serial,\
+ javax.sql.rowset.spi,\
+ javax.swing,\
+ javax.swing.border,\
+ javax.swing.colorchooser,\
+ javax.swing.event,\
+ javax.swing.filechooser,\
+ javax.swing.plaf,\
+ javax.swing.plaf.basic,\
+ javax.swing.plaf.metal,\
+ javax.swing.plaf.multi,\
+ javax.swing.plaf.synth,\
+ javax.swing.table,\
+ javax.swing.text,\
+ javax.swing.text.html,\
+ javax.swing.text.html.parser,\
+ javax.swing.text.rtf,\
+ javax.swing.tree,\
+ javax.swing.undo,\
+ javax.tools,\
+ javax.transaction,\
+ javax.transaction.xa,\
+ javax.xml,\
+ javax.xml.bind,\
+ javax.xml.bind.annotation,\
+ javax.xml.bind.annotation.adapters,\
+ javax.xml.bind.attachment,\
+ javax.xml.bind.helpers,\
+ javax.xml.bind.util,\
+ javax.xml.crypto,\
+ javax.xml.crypto.dom,\
+ javax.xml.crypto.dsig,\
+ javax.xml.crypto.dsig.dom,\
+ javax.xml.crypto.dsig.keyinfo,\
+ javax.xml.crypto.dsig.spec,\
+ javax.xml.datatype,\
+ javax.xml.namespace,\
+ javax.xml.parsers,\
+ javax.xml.soap,\
+ javax.xml.stream,\
+ javax.xml.stream.events,\
+ javax.xml.stream.util,\
+ javax.xml.transform,\
+ javax.xml.transform.dom,\
+ javax.xml.transform.sax,\
+ javax.xml.transform.stax,\
+ javax.xml.transform.stream,\
+ javax.xml.validation,\
+ javax.xml.ws,\
+ javax.xml.ws.handler,\
+ javax.xml.ws.handler.soap,\
+ javax.xml.ws.http,\
+ javax.xml.ws.soap,\
+ javax.xml.ws.spi,\
+ javax.xml.ws.wsaddressing,\
+ javax.xml.xpath,\
+ org.ietf.jgss,\
+ org.omg.CORBA,\
+ org.omg.CORBA_2_3,\
+ org.omg.CORBA_2_3.portable,\
+ org.omg.CORBA.DynAnyPackage,\
+ org.omg.CORBA.ORBPackage,\
+ org.omg.CORBA.portable,\
+ org.omg.CORBA.TypeCodePackage,\
+ org.omg.CosNaming,\
+ org.omg.CosNaming.NamingContextExtPackage,\
+ org.omg.CosNaming.NamingContextPackage,\
+ org.omg.Dynamic,\
+ org.omg.DynamicAny,\
+ org.omg.DynamicAny.DynAnyFactoryPackage,\
+ org.omg.DynamicAny.DynAnyPackage,\
+ org.omg.IOP,\
+ org.omg.IOP.CodecFactoryPackage,\
+ org.omg.IOP.CodecPackage,\
+ org.omg.Messaging,\
+ org.omg.PortableInterceptor,\
+ org.omg.PortableInterceptor.ORBInitInfoPackage,\
+ org.omg.PortableServer,\
+ org.omg.PortableServer.CurrentPackage,\
+ org.omg.PortableServer.POAManagerPackage,\
+ org.omg.PortableServer.POAPackage,\
+ org.omg.PortableServer.portable,\
+ org.omg.PortableServer.ServantLocatorPackage,\
+ org.omg.SendingContext,\
+ org.omg.stub.java.rmi,\
+ org.w3c.dom,\
+ org.w3c.dom.bootstrap,\
+ org.w3c.dom.css,\
+ org.w3c.dom.events,\
+ org.w3c.dom.html,\
+ org.w3c.dom.ls,\
+ org.w3c.dom.ranges,\
+ org.w3c.dom.stylesheets,\
+ org.w3c.dom.traversal,\
+ org.w3c.dom.views,\
+ org.w3c.dom.xpath,\
+ org.xml.sax,\
+ org.xml.sax.ext,\
+ org.xml.sax.helpers
+org.osgi.framework.bootdelegation = \
+ javax.*,\
+ org.ietf.jgss,\
+ org.omg.*,\
+ org.w3c.*,\
+ org.xml.*,\
+ sun.*,\
+ com.sun.*
+org.osgi.framework.executionenvironment = \
+ OSGi/Minimum-1.0,\
+ OSGi/Minimum-1.1,\
+ OSGi/Minimum-1.2,\
+ JRE-1.1,\
+ J2SE-1.2,\
+ J2SE-1.3,\
+ J2SE-1.4,\
+ J2SE-1.5,\
+ JavaSE-1.6
+osgi.java.profile.name = JavaSE-1.6
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/JavaSE-1.7.profile b/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/JavaSE-1.7.profile
new file mode 100644 (file)
index 0000000..192b46e
--- /dev/null
@@ -0,0 +1,198 @@
+###############################################################################
+# Copyright (c) 2009, 2010 IBM Corporation and others.
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+# 
+# Contributors:
+#     IBM Corporation - initial API and implementation
+###############################################################################
+org.osgi.framework.system.packages = \
+ javax.accessibility,\
+ javax.activation,\
+ javax.activity,\
+ javax.annotation,\
+ javax.annotation.processing,\
+ javax.crypto,\
+ javax.crypto.interfaces,\
+ javax.crypto.spec,\
+ javax.imageio,\
+ javax.imageio.event,\
+ javax.imageio.metadata,\
+ javax.imageio.plugins.bmp,\
+ javax.imageio.plugins.jpeg,\
+ javax.imageio.spi,\
+ javax.imageio.stream,\
+ javax.jws,\
+ javax.jws.soap,\
+ javax.lang.model,\
+ javax.lang.model.element,\
+ javax.lang.model.type,\
+ javax.lang.model.util,\
+ javax.management,\
+ javax.management.event,\
+ javax.management.loading,\
+ javax.management.modelmbean,\
+ javax.management.monitor,\
+ javax.management.namespace,\
+ javax.management.openmbean,\
+ javax.management.relation,\
+ javax.management.remote,\
+ javax.management.remote.rmi,\
+ javax.management.timer,\
+ javax.naming,\
+ javax.naming.directory,\
+ javax.naming.event,\
+ javax.naming.ldap,\
+ javax.naming.spi,\
+ javax.net,\
+ javax.net.ssl,\
+ javax.print,\
+ javax.print.attribute,\
+ javax.print.attribute.standard,\
+ javax.print.event,\
+ javax.rmi,\
+ javax.rmi.CORBA,\
+ javax.rmi.ssl,\
+ javax.script,\
+ javax.security.auth,\
+ javax.security.auth.callback,\
+ javax.security.auth.kerberos,\
+ javax.security.auth.login,\
+ javax.security.auth.spi,\
+ javax.security.auth.x500,\
+ javax.security.cert,\
+ javax.security.sasl,\
+ javax.sound.midi,\
+ javax.sound.midi.spi,\
+ javax.sound.sampled,\
+ javax.sound.sampled.spi,\
+ javax.sql,\
+ javax.sql.rowset,\
+ javax.sql.rowset.serial,\
+ javax.sql.rowset.spi,\
+ javax.swing,\
+ javax.swing.border,\
+ javax.swing.colorchooser,\
+ javax.swing.event,\
+ javax.swing.filechooser,\
+ javax.swing.plaf,\
+ javax.swing.plaf.basic,\
+ javax.swing.plaf.metal,\
+ javax.swing.plaf.multi,\
+ javax.swing.plaf.nimbus,\
+ javax.swing.plaf.synth,\
+ javax.swing.table,\
+ javax.swing.text,\
+ javax.swing.text.html,\
+ javax.swing.text.html.parser,\
+ javax.swing.text.rtf,\
+ javax.swing.tree,\
+ javax.swing.undo,\
+ javax.tools,\
+ javax.transaction,\
+ javax.transaction.xa,\
+ javax.xml,\
+ javax.xml.bind,\
+ javax.xml.bind.annotation,\
+ javax.xml.bind.annotation.adapters,\
+ javax.xml.bind.attachment,\
+ javax.xml.bind.helpers,\
+ javax.xml.bind.util,\
+ javax.xml.crypto,\
+ javax.xml.crypto.dom,\
+ javax.xml.crypto.dsig,\
+ javax.xml.crypto.dsig.dom,\
+ javax.xml.crypto.dsig.keyinfo,\
+ javax.xml.crypto.dsig.spec,\
+ javax.xml.datatype,\
+ javax.xml.namespace,\
+ javax.xml.parsers,\
+ javax.xml.soap,\
+ javax.xml.stream,\
+ javax.xml.stream.events,\
+ javax.xml.stream.util,\
+ javax.xml.transform,\
+ javax.xml.transform.dom,\
+ javax.xml.transform.sax,\
+ javax.xml.transform.stax,\
+ javax.xml.transform.stream,\
+ javax.xml.validation,\
+ javax.xml.ws,\
+ javax.xml.ws.handler,\
+ javax.xml.ws.handler.soap,\
+ javax.xml.ws.http,\
+ javax.xml.ws.soap,\
+ javax.xml.ws.spi,\
+ javax.xml.ws.wsaddressing,\
+ javax.xml.xpath,\
+ org.ietf.jgss,\
+ org.omg.CORBA,\
+ org.omg.CORBA_2_3,\
+ org.omg.CORBA_2_3.portable,\
+ org.omg.CORBA.DynAnyPackage,\
+ org.omg.CORBA.ORBPackage,\
+ org.omg.CORBA.portable,\
+ org.omg.CORBA.TypeCodePackage,\
+ org.omg.CosNaming,\
+ org.omg.CosNaming.NamingContextExtPackage,\
+ org.omg.CosNaming.NamingContextPackage,\
+ org.omg.Dynamic,\
+ org.omg.DynamicAny,\
+ org.omg.DynamicAny.DynAnyFactoryPackage,\
+ org.omg.DynamicAny.DynAnyPackage,\
+ org.omg.IOP,\
+ org.omg.IOP.CodecFactoryPackage,\
+ org.omg.IOP.CodecPackage,\
+ org.omg.Messaging,\
+ org.omg.PortableInterceptor,\
+ org.omg.PortableInterceptor.ORBInitInfoPackage,\
+ org.omg.PortableServer,\
+ org.omg.PortableServer.CurrentPackage,\
+ org.omg.PortableServer.POAManagerPackage,\
+ org.omg.PortableServer.POAPackage,\
+ org.omg.PortableServer.portable,\
+ org.omg.PortableServer.ServantLocatorPackage,\
+ org.omg.SendingContext,\
+ org.omg.stub.java.rmi,\
+ org.w3c.dom,\
+ org.w3c.dom.bootstrap,\
+ org.w3c.dom.css,\
+ org.w3c.dom.events,\
+ org.w3c.dom.html,\
+ org.w3c.dom.ls,\
+ org.w3c.dom.ranges,\
+ org.w3c.dom.stylesheets,\
+ org.w3c.dom.traversal,\
+ org.w3c.dom.views,\
+ org.w3c.dom.xpath,\
+ org.xml.sax,\
+ org.xml.sax.ext,\
+ org.xml.sax.helpers
+org.osgi.framework.bootdelegation = \
+ javax.*,\
+ org.ietf.jgss,\
+ org.omg.*,\
+ org.w3c.*,\
+ org.xml.*,\
+ sun.*,\
+ com.sun.*
+org.osgi.framework.executionenvironment = \
+ OSGi/Minimum-1.0,\
+ OSGi/Minimum-1.1,\
+ OSGi/Minimum-1.2,\
+ JRE-1.1,\
+ J2SE-1.2,\
+ J2SE-1.3,\
+ J2SE-1.4,\
+ J2SE-1.5,\
+ JavaSE-1.6,\
+ JavaSE-1.7
+osgi.java.profile.name = JavaSE-1.7
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/MavenWrapper.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/MavenWrapper.java
new file mode 100644 (file)
index 0000000..84d29a8
--- /dev/null
@@ -0,0 +1,122 @@
+package org.argeo.slc.repo.osgi;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.Session;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.DefaultNameVersion;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.repo.OsgiFactory;
+import org.argeo.slc.repo.RepoUtils;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+
+/**
+ * BND wrapper based on a Maven artifact available from one of the configured
+ * repositories.
+ */
+public class MavenWrapper extends BndWrapper implements Runnable {
+       private final static CmsLog log = CmsLog.getLog(MavenWrapper.class);
+
+       private String sourceCoords;
+
+       private OsgiFactory osgiFactory;
+
+       private Boolean doNotModifySources = false;
+
+       public MavenWrapper() {
+               setFactory(this);
+       }
+
+       @Override
+       public String getVersion() {
+               String version = super.getVersion();
+               if (version != null)
+                       return version;
+               return new DefaultArtifact(sourceCoords).getVersion();
+       }
+
+       public void run() {
+               Session distSession = null;
+               Session javaSession = null;
+               InputStream in = null;
+               ByteArrayOutputStream out = null;
+               try {
+                       distSession = osgiFactory.openDistSession();
+                       javaSession = osgiFactory.openJavaSession();
+                       Node origArtifact;
+                       try {
+                               origArtifact = osgiFactory.getMaven(distSession, sourceCoords);
+                       } catch (Exception e1) {
+                               origArtifact = osgiFactory.getMaven(distSession, sourceCoords + ":" + getVersion());
+                       }
+
+                       in = origArtifact.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary().getStream();
+                       out = new ByteArrayOutputStream();
+                       wrapJar(in, out);
+                       Node newJarNode = RepoUtils.copyBytesAsArtifact(javaSession.getRootNode(), getArtifact(),
+                                       out.toByteArray());
+                       osgiFactory.indexNode(newJarNode);
+                       newJarNode.getSession().save();
+
+                       if (log.isDebugEnabled())
+                               log.debug("Wrapped Maven " + sourceCoords + " to " + newJarNode.getPath());
+
+                       // sources
+                       Artifact sourcesArtifact = new SubArtifact(new DefaultArtifact(sourceCoords), "sources", null);
+                       Node sourcesArtifactNode;
+                       try {
+
+                               sourcesArtifactNode = osgiFactory.getMaven(distSession, sourcesArtifact.toString());
+                       } catch (SlcException e) {
+                               // no sources available
+                               return;
+                       }
+
+                       IOUtils.closeQuietly(in);
+                       in = sourcesArtifactNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary().getStream();
+                       byte[] pdeSource;
+                       if (doNotModifySources)
+                               pdeSource = IOUtils.toByteArray(in);
+                       else
+                               pdeSource = RepoUtils.packageAsPdeSource(in, new DefaultNameVersion(getName(), getVersion()));
+                       Node pdeSourceNode = RepoUtils.copyBytesAsArtifact(javaSession.getRootNode(),
+                                       new DefaultArtifact(getCategory(), getName() + ".source", "jar", getVersion()), pdeSource);
+                       osgiFactory.indexNode(pdeSourceNode);
+                       pdeSourceNode.getSession().save();
+
+                       if (log.isDebugEnabled())
+                               log.debug("Wrapped Maven " + sourcesArtifact + " to PDE sources " + pdeSourceNode.getPath());
+               } catch (Exception e) {
+                       throw new SlcException("Cannot wrap Maven " + sourceCoords, e);
+               } finally {
+                       JcrUtils.logoutQuietly(distSession);
+                       JcrUtils.logoutQuietly(javaSession);
+                       IOUtils.closeQuietly(in);
+                       IOUtils.closeQuietly(out);
+               }
+       }
+
+       public void setSourceCoords(String sourceCoords) {
+               this.sourceCoords = sourceCoords;
+       }
+
+       public String getSourceCoords() {
+               return sourceCoords;
+       }
+
+       public void setOsgiFactory(OsgiFactory osgiFactory) {
+               this.osgiFactory = osgiFactory;
+       }
+
+       public void setDoNotModifySources(Boolean doNotModifySources) {
+               this.doNotModifySources = doNotModifySources;
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/NormalizeGroup.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/NormalizeGroup.java
new file mode 100644 (file)
index 0000000..8679c8c
--- /dev/null
@@ -0,0 +1,437 @@
+package org.argeo.slc.repo.osgi;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.TreeSet;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.commons.io.FilenameUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrMonitor;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.argeo.slc.repo.ArtifactIndexer;
+import org.argeo.slc.repo.RepoConstants;
+import org.argeo.slc.repo.RepoUtils;
+import org.argeo.slc.repo.maven.ArtifactIdComparator;
+import org.argeo.slc.repo.maven.MavenConventionsUtils;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+
+/**
+ * Make sure that all JCR metadata and Maven metadata are consistent for this
+ * group of OSGi bundles.
+ * 
+ * The job is now done via the various {@code NodeIndexer} of the
+ * WorkspaceManager. TODO import dependencies in the workspace.
+ */
+@Deprecated
+public class NormalizeGroup implements Runnable, SlcNames {
+       private final static CmsLog log = CmsLog.getLog(NormalizeGroup.class);
+
+       private Repository repository;
+       private String workspace;
+       private String groupId;
+       private Boolean overridePoms = false;
+       private String artifactBasePath = "/";
+       private String version = null;
+       private String parentPomCoordinates;
+
+       private List<String> excludedSuffixes = new ArrayList<String>();
+
+       private ArtifactIndexer artifactIndexer = new ArtifactIndexer();
+       // private JarFileIndexer jarFileIndexer = new JarFileIndexer();
+
+       /** TODO make it more generic */
+       private List<String> systemPackages = OsgiProfile.PROFILE_JAVA_SE_1_6.getSystemPackages();
+
+       // indexes
+       private Map<String, String> packagesToSymbolicNames = new HashMap<String, String>();
+       private Map<String, Node> symbolicNamesToNodes = new HashMap<String, Node>();
+
+       private Set<Artifact> binaries = new TreeSet<Artifact>(new ArtifactIdComparator());
+       private Set<Artifact> sources = new TreeSet<Artifact>(new ArtifactIdComparator());
+
+       public void run() {
+               Session session = null;
+               try {
+                       session = repository.login(workspace);
+                       Node groupNode = session.getNode(MavenConventionsUtils.groupPath(artifactBasePath, groupId));
+                       processGroupNode(groupNode, null);
+               } catch (Exception e) {
+                       throw new SlcException("Cannot normalize group " + groupId + " in " + workspace, e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+       public static void processGroupNode(Node groupNode, String version, Boolean overridePoms, JcrMonitor monitor)
+                       throws RepositoryException {
+               // TODO set artifactsBase based on group node
+               NormalizeGroup ng = new NormalizeGroup();
+               String groupId = groupNode.getProperty(SlcNames.SLC_GROUP_BASE_ID).getString();
+               ng.setGroupId(groupId);
+               ng.setVersion(version);
+               ng.setOverridePoms(overridePoms);
+               ng.processGroupNode(groupNode, monitor);
+       }
+
+       protected void processGroupNode(Node groupNode, JcrMonitor monitor) throws RepositoryException {
+               if (monitor != null)
+                       monitor.subTask("Group " + groupId);
+               Node allArtifactsHighestVersion = null;
+               Session session = groupNode.getSession();
+               aBases: for (NodeIterator aBases = groupNode.getNodes(); aBases.hasNext();) {
+                       Node aBase = aBases.nextNode();
+                       if (aBase.isNodeType(SlcTypes.SLC_ARTIFACT_BASE)) {
+                               Node highestAVersion = null;
+                               for (NodeIterator aVersions = aBase.getNodes(); aVersions.hasNext();) {
+                                       Node aVersion = aVersions.nextNode();
+                                       if (aVersion.isNodeType(SlcTypes.SLC_ARTIFACT_VERSION_BASE)) {
+                                               if (highestAVersion == null) {
+                                                       highestAVersion = aVersion;
+                                                       if (allArtifactsHighestVersion == null)
+                                                               allArtifactsHighestVersion = aVersion;
+
+                                                       // BS will fail if artifacts arrive in this order
+                                                       // Name1 - V1, name2 - V3, V1 will remain the
+                                                       // allArtifactsHighestVersion
+                                                       // Fixed below
+                                                       else {
+                                                               Version currVersion = extractOsgiVersion(aVersion);
+                                                               Version highestVersion = extractOsgiVersion(allArtifactsHighestVersion);
+                                                               if (currVersion.compareTo(highestVersion) > 0)
+                                                                       allArtifactsHighestVersion = aVersion;
+                                                       }
+
+                                               } else {
+                                                       Version currVersion = extractOsgiVersion(aVersion);
+                                                       Version currentHighestVersion = extractOsgiVersion(highestAVersion);
+                                                       if (currVersion.compareTo(currentHighestVersion) > 0) {
+                                                               highestAVersion = aVersion;
+                                                       }
+                                                       if (currVersion.compareTo(extractOsgiVersion(allArtifactsHighestVersion)) > 0) {
+                                                               allArtifactsHighestVersion = aVersion;
+                                                       }
+                                               }
+
+                                       }
+
+                               }
+                               if (highestAVersion == null)
+                                       continue aBases;
+                               for (NodeIterator files = highestAVersion.getNodes(); files.hasNext();) {
+                                       Node file = files.nextNode();
+                                       if (file.isNodeType(SlcTypes.SLC_BUNDLE_ARTIFACT)) {
+                                               preProcessBundleArtifact(file);
+                                               file.getSession().save();
+                                               if (log.isDebugEnabled())
+                                                       log.debug("Pre-processed " + file.getName());
+                                       }
+
+                               }
+                       }
+               }
+
+               // if version not set or empty, use the highest version
+               // useful when indexing a product maven repository where
+               // all artifacts have the same version for a given release
+               // => the version can then be left empty
+               if (version == null || version.trim().equals(""))
+                       if (allArtifactsHighestVersion != null)
+                               version = allArtifactsHighestVersion.getProperty(SLC_ARTIFACT_VERSION).getString();
+                       else
+                               version = "0.0";
+               // throw new SlcException("Group version " + version
+               // + " is empty.");
+
+               int bundleCount = symbolicNamesToNodes.size();
+               if (log.isDebugEnabled())
+                       log.debug("Indexed " + bundleCount + " bundles");
+
+               int count = 1;
+               for (Node bundleNode : symbolicNamesToNodes.values()) {
+                       processBundleArtifact(bundleNode);
+                       bundleNode.getSession().save();
+                       if (log.isDebugEnabled())
+                               log.debug(count + "/" + bundleCount + " Processed " + bundleNode.getName());
+                       count++;
+               }
+
+               // indexes
+               Set<Artifact> indexes = new TreeSet<Artifact>(new ArtifactIdComparator());
+               Artifact indexArtifact = writeIndex(session, RepoConstants.BINARIES_ARTIFACT_ID, binaries);
+               indexes.add(indexArtifact);
+               indexArtifact = writeIndex(session, RepoConstants.SOURCES_ARTIFACT_ID, sources);
+               indexes.add(indexArtifact);
+               // sdk
+               writeIndex(session, RepoConstants.SDK_ARTIFACT_ID, indexes);
+               if (monitor != null)
+                       monitor.worked(1);
+       }
+
+       private Version extractOsgiVersion(Node artifactVersion) throws RepositoryException {
+               String rawVersion = artifactVersion.getProperty(SLC_ARTIFACT_VERSION).getString();
+               String cleanVersion = rawVersion.replace("-SNAPSHOT", ".SNAPSHOT");
+               Version osgiVersion = null;
+               // log invalid version value to enable tracking them
+               try {
+                       osgiVersion = new Version(cleanVersion);
+               } catch (IllegalArgumentException e) {
+                       log.error("Version string " + cleanVersion + " is invalid ");
+                       String twickedVersion = twickInvalidVersion(cleanVersion);
+                       osgiVersion = new Version(twickedVersion);
+                       log.error("Using " + twickedVersion + " instead");
+                       // throw e;
+               }
+               return osgiVersion;
+       }
+
+       private String twickInvalidVersion(String tmpVersion) {
+               String[] tokens = tmpVersion.split("\\.");
+               if (tokens.length == 3 && tokens[2].lastIndexOf("-") > 0) {
+                       String newSuffix = tokens[2].replaceFirst("-", ".");
+                       tmpVersion = tmpVersion.replaceFirst(tokens[2], newSuffix);
+               } else if (tokens.length > 4) {
+                       // FIXME manually remove other "."
+                       StringTokenizer st = new StringTokenizer(tmpVersion, ".", true);
+                       StringBuilder builder = new StringBuilder();
+                       // Major
+                       builder.append(st.nextToken()).append(st.nextToken());
+                       // Minor
+                       builder.append(st.nextToken()).append(st.nextToken());
+                       // Micro
+                       builder.append(st.nextToken()).append(st.nextToken());
+                       // Qualifier
+                       builder.append(st.nextToken());
+                       while (st.hasMoreTokens()) {
+                               // consume delimiter
+                               st.nextToken();
+                               if (st.hasMoreTokens())
+                                       builder.append("-").append(st.nextToken());
+                       }
+                       tmpVersion = builder.toString();
+               }
+               return tmpVersion;
+       }
+
+       private Artifact writeIndex(Session session, String artifactId, Set<Artifact> artifacts)
+                       throws RepositoryException {
+               Artifact artifact = new DefaultArtifact(groupId, artifactId, "pom", version);
+               Artifact parentArtifact = parentPomCoordinates != null ? new DefaultArtifact(parentPomCoordinates) : null;
+               String pom = MavenConventionsUtils.artifactsAsDependencyPom(artifact, artifacts, parentArtifact);
+               Node node = RepoUtils.copyBytesAsArtifact(session.getNode(artifactBasePath), artifact, pom.getBytes());
+               artifactIndexer.index(node);
+
+               // TODO factorize
+               String pomSha = JcrUtils.checksumFile(node, "SHA-1");
+               JcrUtils.copyBytesAsFile(node.getParent(), node.getName() + ".sha1", pomSha.getBytes());
+               String pomMd5 = JcrUtils.checksumFile(node, "MD5");
+               JcrUtils.copyBytesAsFile(node.getParent(), node.getName() + ".md5", pomMd5.getBytes());
+               session.save();
+               return artifact;
+       }
+
+       protected void preProcessBundleArtifact(Node bundleNode) throws RepositoryException {
+
+               String symbolicName = JcrUtils.get(bundleNode, SLC_SYMBOLIC_NAME);
+               if (symbolicName.endsWith(".source")) {
+                       // TODO make a shared node with classifier 'sources'?
+                       String bundleName = RepoUtils.extractBundleNameFromSourceName(symbolicName);
+                       for (String excludedSuffix : excludedSuffixes) {
+                               if (bundleName.endsWith(excludedSuffix))
+                                       return;// skip adding to sources
+                       }
+                       sources.add(RepoUtils.asArtifact(bundleNode));
+                       return;
+               }
+
+               NodeIterator exportPackages = bundleNode.getNodes(SLC_ + Constants.EXPORT_PACKAGE);
+               while (exportPackages.hasNext()) {
+                       Node exportPackage = exportPackages.nextNode();
+                       String pkg = JcrUtils.get(exportPackage, SLC_NAME);
+                       packagesToSymbolicNames.put(pkg, symbolicName);
+               }
+
+               symbolicNamesToNodes.put(symbolicName, bundleNode);
+               for (String excludedSuffix : excludedSuffixes) {
+                       if (symbolicName.endsWith(excludedSuffix))
+                               return;// skip adding to binaries
+               }
+               binaries.add(RepoUtils.asArtifact(bundleNode));
+
+               if (bundleNode.getSession().hasPendingChanges())
+                       bundleNode.getSession().save();
+       }
+
+       protected void processBundleArtifact(Node bundleNode) throws RepositoryException {
+               Node artifactFolder = bundleNode.getParent();
+               String baseName = FilenameUtils.getBaseName(bundleNode.getName());
+
+               // pom
+               String pomName = baseName + ".pom";
+               if (artifactFolder.hasNode(pomName) && !overridePoms)
+                       return;// skip
+
+               String pom = generatePomForBundle(bundleNode);
+               Node pomNode = JcrUtils.copyBytesAsFile(artifactFolder, pomName, pom.getBytes());
+               // checksum
+               String bundleSha = JcrUtils.checksumFile(bundleNode, "SHA-1");
+               JcrUtils.copyBytesAsFile(artifactFolder, bundleNode.getName() + ".sha1", bundleSha.getBytes());
+               String pomSha = JcrUtils.checksumFile(pomNode, "SHA-1");
+               JcrUtils.copyBytesAsFile(artifactFolder, pomNode.getName() + ".sha1", pomSha.getBytes());
+       }
+
+       private String generatePomForBundle(Node n) throws RepositoryException {
+               String ownSymbolicName = JcrUtils.get(n, SLC_SYMBOLIC_NAME);
+
+               StringBuffer p = new StringBuffer();
+
+               // XML header
+               p.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
+               p.append(
+                               "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n");
+               p.append("<modelVersion>4.0.0</modelVersion>");
+
+               // Artifact
+               p.append("<groupId>").append(JcrUtils.get(n, SLC_GROUP_ID)).append("</groupId>\n");
+               p.append("<artifactId>").append(JcrUtils.get(n, SLC_ARTIFACT_ID)).append("</artifactId>\n");
+               p.append("<version>").append(JcrUtils.get(n, SLC_ARTIFACT_VERSION)).append("</version>\n");
+               p.append("<packaging>pom</packaging>\n");
+               if (n.hasProperty(SLC_ + Constants.BUNDLE_NAME))
+                       p.append("<name>").append(JcrUtils.get(n, SLC_ + Constants.BUNDLE_NAME)).append("</name>\n");
+               if (n.hasProperty(SLC_ + Constants.BUNDLE_DESCRIPTION))
+                       p.append("<description>").append(JcrUtils.get(n, SLC_ + Constants.BUNDLE_DESCRIPTION))
+                                       .append("</description>\n");
+
+               // Dependencies
+               Set<String> dependenciesSymbolicNames = new TreeSet<String>();
+               Set<String> optionalSymbolicNames = new TreeSet<String>();
+               NodeIterator importPackages = n.getNodes(SLC_ + Constants.IMPORT_PACKAGE);
+               while (importPackages.hasNext()) {
+                       Node importPackage = importPackages.nextNode();
+                       String pkg = JcrUtils.get(importPackage, SLC_NAME);
+                       if (packagesToSymbolicNames.containsKey(pkg)) {
+                               String dependencySymbolicName = packagesToSymbolicNames.get(pkg);
+                               if (JcrUtils.check(importPackage, SLC_OPTIONAL))
+                                       optionalSymbolicNames.add(dependencySymbolicName);
+                               else
+                                       dependenciesSymbolicNames.add(dependencySymbolicName);
+                       } else {
+                               if (!JcrUtils.check(importPackage, SLC_OPTIONAL) && !systemPackages.contains(pkg))
+                                       log.warn("No bundle found for pkg " + pkg);
+                       }
+               }
+
+               if (n.hasNode(SLC_ + Constants.FRAGMENT_HOST)) {
+                       String fragmentHost = JcrUtils.get(n.getNode(SLC_ + Constants.FRAGMENT_HOST), SLC_SYMBOLIC_NAME);
+                       dependenciesSymbolicNames.add(fragmentHost);
+               }
+
+               // TODO require bundles
+
+               List<Node> dependencyNodes = new ArrayList<Node>();
+               for (String depSymbName : dependenciesSymbolicNames) {
+                       if (depSymbName.equals(ownSymbolicName))
+                               continue;// skip self
+
+                       if (symbolicNamesToNodes.containsKey(depSymbName))
+                               dependencyNodes.add(symbolicNamesToNodes.get(depSymbName));
+                       else
+                               log.warn("Could not find node for " + depSymbName);
+               }
+               List<Node> optionalDependencyNodes = new ArrayList<Node>();
+               for (String depSymbName : optionalSymbolicNames) {
+                       if (symbolicNamesToNodes.containsKey(depSymbName))
+                               optionalDependencyNodes.add(symbolicNamesToNodes.get(depSymbName));
+                       else
+                               log.warn("Could not find node for " + depSymbName);
+               }
+
+               p.append("<dependencies>\n");
+               for (Node dependencyNode : dependencyNodes) {
+                       p.append("<dependency>\n");
+                       p.append("\t<groupId>").append(JcrUtils.get(dependencyNode, SLC_GROUP_ID)).append("</groupId>\n");
+                       p.append("\t<artifactId>").append(JcrUtils.get(dependencyNode, SLC_ARTIFACT_ID)).append("</artifactId>\n");
+                       p.append("</dependency>\n");
+               }
+
+               if (optionalDependencyNodes.size() > 0)
+                       p.append("<!-- OPTIONAL -->\n");
+               for (Node dependencyNode : optionalDependencyNodes) {
+                       p.append("<dependency>\n");
+                       p.append("\t<groupId>").append(JcrUtils.get(dependencyNode, SLC_GROUP_ID)).append("</groupId>\n");
+                       p.append("\t<artifactId>").append(JcrUtils.get(dependencyNode, SLC_ARTIFACT_ID)).append("</artifactId>\n");
+                       p.append("\t<optional>true</optional>\n");
+                       p.append("</dependency>\n");
+               }
+               p.append("</dependencies>\n");
+
+               // Dependency management
+               p.append("<dependencyManagement>\n");
+               p.append("<dependencies>\n");
+               p.append("<dependency>\n");
+               p.append("\t<groupId>").append(groupId).append("</groupId>\n");
+               p.append("\t<artifactId>").append(ownSymbolicName.endsWith(".source") ? RepoConstants.SOURCES_ARTIFACT_ID
+                               : RepoConstants.BINARIES_ARTIFACT_ID).append("</artifactId>\n");
+               p.append("\t<version>").append(version).append("</version>\n");
+               p.append("\t<type>pom</type>\n");
+               p.append("\t<scope>import</scope>\n");
+               p.append("</dependency>\n");
+               p.append("</dependencies>\n");
+               p.append("</dependencyManagement>\n");
+
+               p.append("</project>\n");
+               return p.toString();
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setRepository(Repository repository) {
+               this.repository = repository;
+       }
+
+       public void setWorkspace(String workspace) {
+               this.workspace = workspace;
+       }
+
+       public void setGroupId(String groupId) {
+               this.groupId = groupId;
+       }
+
+       public void setParentPomCoordinates(String parentPomCoordinates) {
+               this.parentPomCoordinates = parentPomCoordinates;
+       }
+
+       public void setArtifactBasePath(String artifactBasePath) {
+               this.artifactBasePath = artifactBasePath;
+       }
+
+       public void setVersion(String version) {
+               this.version = version;
+       }
+
+       public void setExcludedSuffixes(List<String> excludedSuffixes) {
+               this.excludedSuffixes = excludedSuffixes;
+       }
+
+       public void setOverridePoms(Boolean overridePoms) {
+               this.overridePoms = overridePoms;
+       }
+
+       public void setArtifactIndexer(ArtifactIndexer artifactIndexer) {
+               this.artifactIndexer = artifactIndexer;
+       }
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ObrWrapper.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ObrWrapper.java
new file mode 100644 (file)
index 0000000..4ace647
--- /dev/null
@@ -0,0 +1,5 @@
+package org.argeo.slc.repo.osgi;
+
+public class ObrWrapper {
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/OsgiFactoryImpl.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/OsgiFactoryImpl.java
new file mode 100644 (file)
index 0000000..a844f20
--- /dev/null
@@ -0,0 +1,238 @@
+package org.argeo.slc.repo.osgi;
+
+import java.io.BufferedInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.security.Privilege;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcConstants;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.argeo.slc.repo.NodeIndexer;
+import org.argeo.slc.repo.OsgiFactory;
+import org.argeo.slc.repo.RepoConstants;
+import org.argeo.slc.repo.maven.MavenConventionsUtils;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+
+/** Default implementation of {@link OsgiFactory}. */
+public class OsgiFactoryImpl implements OsgiFactory, SlcNames {
+       private final static CmsLog log = CmsLog.getLog(OsgiFactoryImpl.class);
+
+       private String workspace;
+       private Repository distRepository;
+       private Repository javaRepository;
+
+       private List<NodeIndexer> nodeIndexers = new ArrayList<NodeIndexer>();
+
+       /** key is URI prefix, value list of base URLs */
+       private Map<String, List<String>> mirrors = new HashMap<String, List<String>>();
+
+       private List<String> mavenRepositories = new ArrayList<String>();
+       private String downloadBase = RepoConstants.DIST_DOWNLOAD_BASEPATH;
+       private String mavenProxyBase = downloadBase + "/maven";
+
+       public void init() {
+               if (workspace == null)
+                       throw new SlcException("A workspace must be specified");
+
+               // default Maven repo
+               if (mavenRepositories.size() == 0) {
+                       // mavenRepositories
+                       // .add("http://search.maven.org/remotecontent?filepath=");
+                       mavenRepositories.add("http://repo1.maven.org/maven2");
+               }
+
+               Session javaSession = null;
+               Session distSession = null;
+               try {
+                       // TODO rather user a JavaRepoManager that will also implicitely
+                       // manage the indexing of newly created nodes.
+                       javaSession = JcrUtils.loginOrCreateWorkspace(javaRepository, workspace);
+                       distSession = JcrUtils.loginOrCreateWorkspace(distRepository, workspace);
+
+                       // Privileges
+                       JcrUtils.addPrivilege(javaSession, "/", SlcConstants.ROLE_SLC, Privilege.JCR_ALL);
+                       JcrUtils.addPrivilege(distSession, "/", SlcConstants.ROLE_SLC, Privilege.JCR_ALL);
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot initialize OSGi Factory " + workspace, e);
+               } finally {
+                       JcrUtils.logoutQuietly(javaSession);
+                       JcrUtils.logoutQuietly(distSession);
+               }
+       }
+
+       public void destroy() {
+
+       }
+
+       public Session openJavaSession() throws RepositoryException {
+               return javaRepository.login(workspace);
+       }
+
+       public Session openDistSession() throws RepositoryException {
+               return distRepository.login(workspace);
+       }
+
+       public void indexNode(Node node) {
+               for (NodeIndexer nodeIndexer : nodeIndexers) {
+                       nodeIndexer.index(node);
+               }
+       }
+
+       public Node getMaven(Session distSession, String coords) throws RepositoryException {
+               Artifact artifact = new DefaultArtifact(coords);
+               String path = MavenConventionsUtils.artifactPath(mavenProxyBase, artifact);
+
+               // exists
+               if (distSession.itemExists(path))
+                       return distSession.getNode(path);
+
+               for (String mavenRepo : mavenRepositories) {
+                       String url = MavenConventionsUtils.artifactUrl(mavenRepo, artifact);
+                       try {
+                               Node node = loadUrlToPath(url, distSession, path);
+                               if (node != null) {
+                                       // checksums
+                                       try {
+                                               loadUrlToPath(url + ".md5", distSession, path + ".md5");
+                                       } catch (FileNotFoundException e) {
+                                               // silent
+                                       }
+                                       try {
+                                               loadUrlToPath(url + ".sha1", distSession, path + ".sha1");
+                                       } catch (FileNotFoundException e) {
+                                               // silent
+                                       }
+                                       return node;
+                               }
+                       } catch (FileNotFoundException e) {
+                               if (log.isDebugEnabled())
+                                       log.debug("Maven " + coords + " could not be downloaded from " + url);
+                       }
+               }
+               throw new SlcException("Could not download Maven " + coords);
+       }
+
+       public Node getDist(Session distSession, String uri) throws RepositoryException {
+               String distPath = downloadBase + '/' + JcrUtils.urlAsPath(uri);
+
+               // already retrieved
+               if (distSession.itemExists(distPath))
+                       return distSession.getNode(distPath);
+
+               // find mirror
+               List<String> urlBases = null;
+               String uriPrefix = null;
+               uriPrefixes: for (String uriPref : mirrors.keySet()) {
+                       if (uri.startsWith(uriPref)) {
+                               if (mirrors.get(uriPref).size() > 0) {
+                                       urlBases = mirrors.get(uriPref);
+                                       uriPrefix = uriPref;
+                                       break uriPrefixes;
+                               }
+                       }
+               }
+               if (urlBases == null)
+                       try {
+                               return loadUrlToPath(uri, distSession, distPath);
+                       } catch (FileNotFoundException e) {
+                               throw new SlcException("Cannot download " + uri, e);
+                       }
+
+               // try to download
+               for (String urlBase : urlBases) {
+                       String relativePath = uri.substring(uriPrefix.length());
+                       String url = urlBase + relativePath;
+                       try {
+                               return loadUrlToPath(url, distSession, distPath);
+                       } catch (FileNotFoundException e) {
+                               if (log.isDebugEnabled())
+                                       log.debug("Cannot download " + url + ", trying another mirror");
+                       }
+               }
+
+               throw new SlcException("Could not download " + uri);
+       }
+
+       /** Actually downloads a file to an internal location */
+       protected Node loadUrlToPath(String url, Session distSession, String path)
+                       throws RepositoryException, FileNotFoundException {
+               if (log.isDebugEnabled())
+                       log.debug("Downloading " + url + "...");
+
+               InputStream in = null;
+               URLConnection conn = null;
+               Node folderNode = JcrUtils.mkfolders(distSession, JcrUtils.parentPath(path));
+               try {
+                       URL u = new URL(url);
+                       conn = u.openConnection();
+                       conn.connect();
+                       in = new BufferedInputStream(conn.getInputStream());
+                       // byte[] arr = IOUtils.toByteArray(in);
+                       // Node fileNode = JcrUtils.copyBytesAsFile(folderNode,
+                       // JcrUtils.nodeNameFromPath(path), arr);
+                       Node fileNode = JcrUtils.copyStreamAsFile(folderNode, JcrUtils.nodeNameFromPath(path), in);
+                       fileNode.addMixin(SlcTypes.SLC_KNOWN_ORIGIN);
+                       Node origin = fileNode.addNode(SLC_ORIGIN, SlcTypes.SLC_PROXIED);
+                       JcrUtils.urlToAddressProperties(origin, url);
+                       distSession.save();
+                       return fileNode;
+               } catch (MalformedURLException e) {
+                       throw new SlcException("URL " + url + " not valid.", e);
+               } catch (FileNotFoundException e) {
+                       throw e;
+               } catch (IOException e) {
+                       throw new SlcException("Cannot load " + url + " to " + path, e);
+               } finally {
+                       IOUtils.closeQuietly(in);
+               }
+
+       }
+
+       public void setWorkspace(String workspace) {
+               this.workspace = workspace;
+       }
+
+       public void setDistRepository(Repository distRepository) {
+               this.distRepository = distRepository;
+       }
+
+       public void setJavaRepository(Repository javaRepository) {
+               this.javaRepository = javaRepository;
+       }
+
+       public void setNodeIndexers(List<NodeIndexer> nodeIndexers) {
+               this.nodeIndexers = nodeIndexers;
+       }
+
+       public void setMirrors(Map<String, List<String>> mirrors) {
+               this.mirrors = mirrors;
+       }
+
+       public void setMavenRepositories(List<String> mavenRepositories) {
+               this.mavenRepositories = mavenRepositories;
+       }
+
+       public void setMavenProxyBase(String mavenProxyBase) {
+               this.mavenProxyBase = mavenProxyBase;
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/OsgiProfile.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/OsgiProfile.java
new file mode 100644 (file)
index 0000000..0d97c98
--- /dev/null
@@ -0,0 +1,51 @@
+package org.argeo.slc.repo.osgi;
+
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.slc.SlcException;
+
+/**
+ * Wraps an OSGi profile, simplifying access to its values such as system
+ * packages, etc.
+ */
+public class OsgiProfile {
+       public final static String PROP_SYSTEM_PACKAGES = "org.osgi.framework.system.packages";
+
+       public final static OsgiProfile PROFILE_JAVA_SE_1_6 = new OsgiProfile("JavaSE-1.6.profile");
+
+       private final URL url;
+       private final Properties properties;
+
+       public OsgiProfile(URL url) {
+               this.url = url;
+               properties = new Properties();
+               InputStream in = null;
+               try {
+                       properties.load(this.url.openStream());
+               } catch (Exception e) {
+                       throw new SlcException("Cannot initalize OSGi profile " + url, e);
+               } finally {
+                       IOUtils.closeQuietly(in);
+               }
+       }
+
+       public OsgiProfile(String name) {
+               this(OsgiProfile.class.getClassLoader()
+                               .getResource('/' + OsgiProfile.class.getPackage().getName().replace('.', '/') + '/' + name));
+       }
+
+       public List<String> getSystemPackages() {
+               String[] splitted = properties.getProperty(PROP_SYSTEM_PACKAGES).split(",");
+               List<String> res = new ArrayList<String>();
+               for (String pkg : splitted) {
+                       res.add(pkg.trim());
+               }
+               return Collections.unmodifiableList(res);
+       }
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ProcessDistribution.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/ProcessDistribution.java
new file mode 100644 (file)
index 0000000..0c65814
--- /dev/null
@@ -0,0 +1,90 @@
+package org.argeo.slc.repo.osgi;
+
+import java.util.Iterator;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.CategoryNameVersion;
+import org.argeo.slc.NameVersion;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.repo.ArgeoOsgiDistribution;
+import org.argeo.slc.repo.ModularDistributionFactory;
+import org.argeo.slc.repo.OsgiFactory;
+import org.argeo.slc.repo.maven.MavenConventionsUtils;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+
+/**
+ * Executes the processes required so that all managed bundles are available.
+ */
+public class ProcessDistribution implements Runnable {
+       private final static CmsLog log = CmsLog.getLog(ProcessDistribution.class);
+
+       private ArgeoOsgiDistribution osgiDistribution;
+       private OsgiFactory osgiFactory;
+
+       public void run() {
+               Session javaSession = null;
+               try {
+                       javaSession = osgiFactory.openJavaSession();
+                       for (Iterator<? extends NameVersion> it = osgiDistribution.nameVersions(); it.hasNext();)
+                               processNameVersion(javaSession, it.next());
+
+                       // Check sources
+                       for (Iterator<? extends NameVersion> it = osgiDistribution.nameVersions(); it.hasNext();) {
+                               CategoryNameVersion nv = (CategoryNameVersion) it.next();
+                               Artifact artifact = new DefaultArtifact(nv.getCategory(), nv.getName() + ".source", "jar",
+                                               nv.getVersion());
+                               String path = MavenConventionsUtils.artifactPath("/", artifact);
+                               if (!javaSession.itemExists(path))
+                                       log.warn("No source available for " + nv);
+                       }
+
+                       // explicitly create the corresponding modular distribution as we
+                       // have here all necessary info.
+                       ModularDistributionFactory mdf = new ModularDistributionFactory(osgiFactory, osgiDistribution);
+                       mdf.run();
+
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot process distribution " + osgiDistribution, e);
+               } finally {
+                       JcrUtils.logoutQuietly(javaSession);
+               }
+       }
+
+       protected void processNameVersion(Session javaSession, NameVersion nameVersion) throws RepositoryException {
+               if (log.isTraceEnabled())
+                       log.trace("Check " + nameVersion + "...");
+               if (!(nameVersion instanceof CategoryNameVersion))
+                       throw new SlcException("Unsupported type " + nameVersion.getClass());
+               CategoryNameVersion nv = (CategoryNameVersion) nameVersion;
+               Artifact artifact = new DefaultArtifact(nv.getCategory(), nv.getName(), "jar", nv.getVersion());
+               String path = MavenConventionsUtils.artifactPath("/", artifact);
+               if (!javaSession.itemExists(path)) {
+                       if (nv instanceof BndWrapper) {
+                               if (log.isDebugEnabled())
+                                       log.debug("Run factory for   : " + nv + "...");
+                               ((BndWrapper) nv).getFactory().run();
+                       } else if (nv instanceof Runnable) {
+                               ((Runnable) nv).run();
+                       } else {
+                               log.warn("Skip unsupported   : " + nv);
+                       }
+               } else {
+                       if (log.isTraceEnabled())
+                               log.trace("Already available : " + nv);
+               }
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setOsgiDistribution(ArgeoOsgiDistribution osgiDistribution) {
+               this.osgiDistribution = osgiDistribution;
+       }
+
+       public void setOsgiFactory(OsgiFactory osgiFactory) {
+               this.osgiFactory = osgiFactory;
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/SourcesProvider.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/SourcesProvider.java
new file mode 100644 (file)
index 0000000..a3b3fc9
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.slc.repo.osgi;
+
+import java.util.List;
+import java.util.zip.ZipOutputStream;
+
+/** Provides access to Java sources */
+public interface SourcesProvider {
+       /**
+        * Writes sources into a ZIP (or a JAR), under the same sirectory structure.
+        * 
+        * @param packages the packages to import
+        * @param out      the ZIP or JAR to write to
+        */
+       public void writeSources(List<String> packages, ZipOutputStream zout);
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/SubArtifact.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/SubArtifact.java
new file mode 100644 (file)
index 0000000..4c567f8
--- /dev/null
@@ -0,0 +1,204 @@
+package org.argeo.slc.repo.osgi;
+
+import java.io.File;
+import java.util.Map;
+
+import org.eclipse.aether.artifact.AbstractArtifact;
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * An artifact whose identity is derived from another artifact. <em>Note:</em>
+ * Instances of this class are immutable and the exposed mutators return new
+ * objects rather than changing the current instance.
+ */
+final class SubArtifact extends AbstractArtifact {
+
+       private final Artifact mainArtifact;
+
+       private final String classifier;
+
+       private final String extension;
+
+       private final File file;
+
+       private final Map<String, String> properties;
+
+       /**
+        * Creates a new sub artifact. The classifier and extension specified for this
+        * artifact may use the asterisk character "*" to refer to the corresponding
+        * property of the main artifact. For instance, the classifier "*-sources" can
+        * be used to refer to the source attachment of an artifact. Likewise, the
+        * extension "*.asc" can be used to refer to the GPG signature of an artifact.
+        * 
+        * @param mainArtifact The artifact from which to derive the identity, must not
+        *                     be {@code null}.
+        * @param classifier   The classifier for this artifact, may be {@code null} if
+        *                     none.
+        * @param extension    The extension for this artifact, may be {@code null} if
+        *                     none.
+        */
+       public SubArtifact(Artifact mainArtifact, String classifier, String extension) {
+               this(mainArtifact, classifier, extension, (File) null);
+       }
+
+       /**
+        * Creates a new sub artifact. The classifier and extension specified for this
+        * artifact may use the asterisk character "*" to refer to the corresponding
+        * property of the main artifact. For instance, the classifier "*-sources" can
+        * be used to refer to the source attachment of an artifact. Likewise, the
+        * extension "*.asc" can be used to refer to the GPG signature of an artifact.
+        * 
+        * @param mainArtifact The artifact from which to derive the identity, must not
+        *                     be {@code null}.
+        * @param classifier   The classifier for this artifact, may be {@code null} if
+        *                     none.
+        * @param extension    The extension for this artifact, may be {@code null} if
+        *                     none.
+        * @param file         The file for this artifact, may be {@code null} if
+        *                     unresolved.
+        */
+       public SubArtifact(Artifact mainArtifact, String classifier, String extension, File file) {
+               this(mainArtifact, classifier, extension, null, file);
+       }
+
+       /**
+        * Creates a new sub artifact. The classifier and extension specified for this
+        * artifact may use the asterisk character "*" to refer to the corresponding
+        * property of the main artifact. For instance, the classifier "*-sources" can
+        * be used to refer to the source attachment of an artifact. Likewise, the
+        * extension "*.asc" can be used to refer to the GPG signature of an artifact.
+        * 
+        * @param mainArtifact The artifact from which to derive the identity, must not
+        *                     be {@code null}.
+        * @param classifier   The classifier for this artifact, may be {@code null} if
+        *                     none.
+        * @param extension    The extension for this artifact, may be {@code null} if
+        *                     none.
+        * @param properties   The properties of the artifact, may be {@code null}.
+        */
+       public SubArtifact(Artifact mainArtifact, String classifier, String extension, Map<String, String> properties) {
+               this(mainArtifact, classifier, extension, properties, null);
+       }
+
+       /**
+        * Creates a new sub artifact. The classifier and extension specified for this
+        * artifact may use the asterisk character "*" to refer to the corresponding
+        * property of the main artifact. For instance, the classifier "*-sources" can
+        * be used to refer to the source attachment of an artifact. Likewise, the
+        * extension "*.asc" can be used to refer to the GPG signature of an artifact.
+        * 
+        * @param mainArtifact The artifact from which to derive the identity, must not
+        *                     be {@code null}.
+        * @param classifier   The classifier for this artifact, may be {@code null} if
+        *                     none.
+        * @param extension    The extension for this artifact, may be {@code null} if
+        *                     none.
+        * @param properties   The properties of the artifact, may be {@code null}.
+        * @param file         The file for this artifact, may be {@code null} if
+        *                     unresolved.
+        */
+       public SubArtifact(Artifact mainArtifact, String classifier, String extension, Map<String, String> properties,
+                       File file) {
+               if (mainArtifact == null) {
+                       throw new IllegalArgumentException("no artifact specified");
+               }
+               this.mainArtifact = mainArtifact;
+               this.classifier = classifier;
+               this.extension = extension;
+               this.file = file;
+               this.properties = copyProperties(properties);
+       }
+
+       private SubArtifact(Artifact mainArtifact, String classifier, String extension, File file,
+                       Map<String, String> properties) {
+               // NOTE: This constructor assumes immutability of the provided properties, for
+               // internal use only
+               this.mainArtifact = mainArtifact;
+               this.classifier = classifier;
+               this.extension = extension;
+               this.file = file;
+               this.properties = properties;
+       }
+
+       public String getGroupId() {
+               return mainArtifact.getGroupId();
+       }
+
+       public String getArtifactId() {
+               return mainArtifact.getArtifactId();
+       }
+
+       public String getVersion() {
+               return mainArtifact.getVersion();
+       }
+
+       public String getBaseVersion() {
+               return mainArtifact.getBaseVersion();
+       }
+
+       public boolean isSnapshot() {
+               return mainArtifact.isSnapshot();
+       }
+
+       public String getClassifier() {
+               return expand(classifier, mainArtifact.getClassifier());
+       }
+
+       public String getExtension() {
+               return expand(extension, mainArtifact.getExtension());
+       }
+
+       public File getFile() {
+               return file;
+       }
+
+       public Artifact setFile(File file) {
+               if ((this.file == null) ? file == null : this.file.equals(file)) {
+                       return this;
+               }
+               return new SubArtifact(mainArtifact, classifier, extension, file, properties);
+       }
+
+       public Map<String, String> getProperties() {
+               return properties;
+       }
+
+       public Artifact setProperties(Map<String, String> properties) {
+               if (this.properties.equals(properties) || (properties == null && this.properties.isEmpty())) {
+                       return this;
+               }
+               return new SubArtifact(mainArtifact, classifier, extension, properties, file);
+       }
+
+       private static String expand(String pattern, String replacement) {
+               String result = "";
+               if (pattern != null) {
+                       result = pattern.replace("*", replacement);
+
+                       if (replacement.length() <= 0) {
+                               if (pattern.startsWith("*")) {
+                                       int i = 0;
+                                       for (; i < result.length(); i++) {
+                                               char c = result.charAt(i);
+                                               if (c != '-' && c != '.') {
+                                                       break;
+                                               }
+                                       }
+                                       result = result.substring(i);
+                               }
+                               if (pattern.endsWith("*")) {
+                                       int i = result.length() - 1;
+                                       for (; i >= 0; i--) {
+                                               char c = result.charAt(i);
+                                               if (c != '-' && c != '.') {
+                                                       break;
+                                               }
+                                       }
+                                       result = result.substring(0, i + 1);
+                               }
+                       }
+               }
+               return result;
+       }
+
+}
\ No newline at end of file
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/UriWrapper.java b/org.argeo.slc.repo/src/org/argeo/slc/repo/osgi/UriWrapper.java
new file mode 100644 (file)
index 0000000..e2c785a
--- /dev/null
@@ -0,0 +1,141 @@
+package org.argeo.slc.repo.osgi;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.List;
+import java.util.zip.ZipOutputStream;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.Session;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.DefaultNameVersion;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.repo.OsgiFactory;
+import org.argeo.slc.repo.RepoUtils;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+
+import aQute.bnd.osgi.Jar;
+
+public class UriWrapper extends BndWrapper implements Runnable {
+       private final static CmsLog log = CmsLog.getLog(UriWrapper.class);
+
+       private String uri;
+       private String baseUri;
+       private String versionSeparator = "-";
+       private String extension = "jar";
+
+       private OsgiFactory osgiFactory;
+
+       private SourcesProvider sourcesProvider;
+
+       public UriWrapper() {
+               setFactory(this);
+       }
+
+       public void run() {
+               Session distSession = null;
+               Session javaSession = null;
+               InputStream in = null;
+               ByteArrayOutputStream out = null;
+               Jar jar = null;
+               try {
+                       distSession = osgiFactory.openDistSession();
+                       javaSession = osgiFactory.openJavaSession();
+                       String uri = getEffectiveUri();
+//                     if (uri == null) {
+//                             uri = baseUri + '/' + getName() + versionSeparator + getVersion() + "." + extension;
+//                     }
+                       Node sourceArtifact = osgiFactory.getDist(distSession, uri);
+
+                       // TODO factorize with Maven
+                       in = sourceArtifact.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary().getStream();
+                       out = new ByteArrayOutputStream();
+                       wrapJar(in, out);
+                       Node newJarNode = RepoUtils.copyBytesAsArtifact(javaSession.getRootNode(), getArtifact(),
+                                       out.toByteArray());
+                       osgiFactory.indexNode(newJarNode);
+                       newJarNode.getSession().save();
+                       if (log.isDebugEnabled())
+                               log.debug("Wrapped " + uri + " to " + newJarNode.getPath());
+
+                       // sources
+                       if (sourcesProvider != null) {
+                               IOUtils.closeQuietly(in);
+                               in = new ByteArrayInputStream(out.toByteArray());
+                               jar = new Jar(null, in);
+                               List<String> packages = jar.getPackages();
+
+                               IOUtils.closeQuietly(out);
+                               out = new ByteArrayOutputStream();
+                               sourcesProvider.writeSources(packages, new ZipOutputStream(out));
+
+                               IOUtils.closeQuietly(in);
+                               in = new ByteArrayInputStream(out.toByteArray());
+                               byte[] sourcesJar = RepoUtils.packageAsPdeSource(in, new DefaultNameVersion(this));
+                               Artifact sourcesArtifact = new DefaultArtifact(getArtifact().getGroupId(),
+                                               getArtifact().getArtifactId() + ".source", "jar", getArtifact().getVersion());
+                               Node sourcesJarNode = RepoUtils.copyBytesAsArtifact(javaSession.getRootNode(), sourcesArtifact,
+                                               sourcesJar);
+                               sourcesJarNode.getSession().save();
+
+                               if (log.isDebugEnabled())
+                                       log.debug("Added sources " + sourcesArtifact + " for bundle " + getArtifact());
+                       }
+               } catch (Exception e) {
+                       throw new SlcException("Cannot wrap URI " + uri, e);
+               } finally {
+                       IOUtils.closeQuietly(in);
+                       IOUtils.closeQuietly(out);
+                       JcrUtils.logoutQuietly(distSession);
+                       JcrUtils.logoutQuietly(javaSession);
+                       if (jar != null)
+                               jar.close();
+               }
+       }
+
+       public void setUri(String sourceCoords) {
+               this.uri = sourceCoords;
+       }
+
+       public String getEffectiveUri() {
+               if (uri == null) {
+                       return baseUri + '/' + getName() + versionSeparator + getVersion() + "." + extension;
+               } else
+                       return uri;
+       }
+
+       public void setOsgiFactory(OsgiFactory osgiFactory) {
+               this.osgiFactory = osgiFactory;
+       }
+
+       public void setBaseUri(String baseUri) {
+               this.baseUri = baseUri;
+       }
+
+       public void setVersionSeparator(String versionSeparator) {
+               this.versionSeparator = versionSeparator;
+       }
+
+       public void setSourcesProvider(SourcesProvider sourcesProvider) {
+               this.sourcesProvider = sourcesProvider;
+       }
+
+       public String getUri() {
+               return uri;
+       }
+
+       public String getBaseUri() {
+               return baseUri;
+       }
+
+       public String getVersionSeparator() {
+               return versionSeparator;
+       }
+
+}
diff --git a/org.argeo.slc.repo/src/org/argeo/slc/repo/repo.cnd b/org.argeo.slc.repo/src/org/argeo/slc/repo/repo.cnd
new file mode 100644 (file)
index 0000000..dacffbb
--- /dev/null
@@ -0,0 +1,164 @@
+<repo = 'http://www.argeo.org/ns/repo'>
+
+// Argeo Commons 1 node types
+[argeo:references] > nt:unstructured
+- * (REFERENCE) *
+
+// AETHER
+[slc:artifact] > mix:referenceable, mix:created, mix:lastModified
+mixin
+- slc:artifactId (STRING) m
+- slc:groupId (STRING) m
+- slc:artifactVersion (STRING) m
+- slc:artifactExtension (STRING) m
+- slc:artifactClassifier (STRING) ='' m a
+
+[slc:artifactVersion] > mix:referenceable, mix:created, mix:lastModified, mix:title
+mixin
+- slc:artifactId (STRING) m
+- slc:groupId (STRING) m
+- slc:artifactVersion (STRING) m
+
+[slc:artifactBase] > mix:referenceable, mix:created, mix:lastModified
+mixin
+- slc:artifactId (STRING) m
+- slc:groupId (STRING) m
+
+[slc:groupBase] > mix:referenceable, mix:created, mix:lastModified
+mixin
+// it is possible to have groupBase being artifact base (e.g. org.argeo.commons.basic)
+// so using groupId would conflict 
+- slc:groupBaseId (STRING) m
+
+// Mark a given group base as relevant to create modular distribution in the current workspace  
+// [slc:category]
+// mixin
+
+[slc:distribution] > slc:artifactVersion
+mixin
++ slc:artifactVersions (argeo:references) m
+
+
+[slc:modularDistributionBase]
+mixin
+
+// Question: Extend slc:categorizedNameVersion ? (not possible without migration)
+[slc:modularDistribution] 
+mixin
++ slc:modules (nt:unstructured) m
+
+[slc:moduleCoordinates] > nt:unstructured
+- slc:category (STRING)
+- slc:name (STRING)
+- slc:version (STRING)
+
+
+// ORIGINS
+[slc:knownOrigin] > nt:base
+mixin
++ slc:origin (nt:address)
+
+[slc:proxied] > nt:address
+- slc:proxy (REFERENCE)
+
+// JAVA
+[slc:jarFile] > mix:referenceable
+mixin
+- 'slc:manifest' (BINARY) m
+- 'slc:Manifest-Version' (STRING)
+- 'slc:Signature-Version' (STRING)
+- 'slc:Class-Path'  (STRING)
+- 'slc:Main-Class' (STRING)
+- 'slc:Extension-Name' (STRING)
+- 'slc:Implementation-Version' (STRING)
+- 'slc:Implementation-Vendor' (STRING)
+- 'slc:Implementation-Vendor-Id' (STRING)
+- 'slc:Implementation-URL' (STRING)
+- 'slc:Specification-Title' (STRING)
+- 'slc:Specification-Version' (STRING)
+- 'slc:Specification-Vendor' (STRING)
+- 'slc:Sealed' (STRING)
+
+// OSGi
+// see http://www.osgi.org/Specifications/Reference
+
+[slc:javaPackage] > mix:referenceable
+- slc:name (STRING) primary m
+
+[slc:osgiBaseVersion] > mix:referenceable
+- slc:asString (STRING) primary m
+- slc:major (LONG) m
+- slc:minor (LONG) m
+- slc:micro (LONG) m
+
+[slc:osgiVersion] > slc:osgiBaseVersion
+- slc:qualifier (STRING)
+
+[slc:exportedPackage] > slc:javaPackage
++ slc:uses (slc:javaPackage) multiple
++ slc:version (slc:osgiVersion)
+
+[slc:importedPackage] > slc:javaPackage
+- slc:version (STRING) ='0.0.0' m a
+- slc:optional (BOOLEAN) ='false' m a
+
+[slc:dynamicImportedPackage] > slc:javaPackage
+- slc:version (STRING) ='0.0.0' m a
+- slc:optional (BOOLEAN) ='false' m a
+
+[slc:requiredBundle] > mix:referenceable
+- 'slc:symbolic-name' (STRING) primary m
+- 'slc:bundle-version' (STRING) ='0.0.0' m a
+- slc:optional (BOOLEAN) ='false' m a
+
+[slc:fragmentHost] > mix:referenceable
+- 'slc:symbolic-name' (STRING) m
+- 'slc:bundle-version' (STRING) ='0.0.0' m a
+
+[slc:bundleNativeCode] > mix:referenceable
+- slc:path (STRING) primary m
+- slc:osname (STRING)
+- slc:processor (STRING)
+
+// see http://www.osgi.org/Specifications/ReferenceHeaders
+[slc:bundle] > mix:referenceable
+mixin
+- 'slc:symbolic-name' (STRING) primary m
+- 'slc:bundle-version' (STRING) m
+- 'slc:Bundle-SymbolicName' (STRING) m
+- 'slc:Bundle-Name' (STRING)
+- 'slc:Bundle-Description' (STRING)
+- 'slc:Bundle-ManifestVersion' (STRING)
+- 'slc:Bundle-Category' (STRING)
+- 'slc:Bundle-ActivationPolicy' (STRING)
+- 'slc:Bundle-Copyright' (STRING)
+- 'slc:Bundle-Vendor' (STRING)
+- 'slc:Bundle-License' (STRING)
+- 'slc:Bundle-DocURL' (STRING)
+- 'slc:Bundle-ContactAddress' (STRING)
+- 'slc:Bundle-Activator' (STRING)
+- 'slc:Bundle-UpdateLocation' (STRING)
+- 'slc:Bundle-Localization' (STRING)
+- 'slc:Bundle-ClassPath' (STRING) *
+// see http://wiki.eclipse.org/EE  < 'OSGi/Minimum-1.0','OSGi/Minimum-1.1','CDC-1.0/Foundation-1.0','CDC-1.1/Foundation-1.1','JRE-1.1','J2SE-1.2','J2SE-1.3','J2SE-1.4','J2SE-1.5','JavaSE-1.6','JavaSE-1.7'
+- 'slc:Bundle-RequiredExecutionEnvironment' (STRING) *
++ 'slc:Bundle-Version' (slc:osgiVersion) m
++ 'slc:Fragment-Host' (slc:fragmentHost)
++ 'slc:Import-Package' (slc:importedPackage) multiple
++ 'slc:Export-Package' (slc:exportedPackage) multiple
++ 'slc:Require-Bundle' (slc:requiredBundle) multiple
++ 'slc:Bundle-NativeCode' (slc:bundleNativeCode) multiple
++ 'slc:DynamicImport-Package' (slc:dynamicImportedPackage) multiple
+
+[slc:bundleArtifact] > slc:artifact,slc:jarFile,slc:bundle
+mixin
+
+// RPM
+[slc:rpm] > mix:referenceable, mix:created, mix:lastModified, mix:title
+mixin
+- slc:name (STRING)
+- slc:version (STRING)
+- slc:rpmVersion (STRING)
+- slc:rpmRelease (STRING)
+- slc:rpmArch (STRING)
+- slc:rpmArchivaeSize (STRING)
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/AbstractForwardingRepositorySystemSession.java b/org.argeo.slc.repo/src/org/eclipse/aether/AbstractForwardingRepositorySystemSession.java
new file mode 100644 (file)
index 0000000..590ad28
--- /dev/null
@@ -0,0 +1,180 @@
+/*******************************************************************************
+ * Copyright (c) 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether;
+
+import java.util.Map;
+
+import org.eclipse.aether.artifact.ArtifactTypeRegistry;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.collection.DependencyManager;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.collection.DependencyTraverser;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.repository.AuthenticationSelector;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.MirrorSelector;
+import org.eclipse.aether.repository.ProxySelector;
+import org.eclipse.aether.repository.WorkspaceReader;
+import org.eclipse.aether.resolution.ArtifactDescriptorPolicy;
+import org.eclipse.aether.resolution.ResolutionErrorPolicy;
+import org.eclipse.aether.transfer.TransferListener;
+
+/**
+ * A special repository system session to enable decorating or proxying another session. To do so, clients have to
+ * create a subclass and implement {@link #getSession()}.
+ */
+public abstract class AbstractForwardingRepositorySystemSession
+    implements RepositorySystemSession
+{
+
+    /**
+     * Creates a new forwarding session.
+     */
+    protected AbstractForwardingRepositorySystemSession()
+    {
+    }
+
+    /**
+     * Gets the repository system session to which this instance forwards calls. It's worth noting that this class does
+     * not save/cache the returned reference but queries this method before each forwarding. Hence, the session
+     * forwarded to may change over time or depending on the context (e.g. calling thread).
+     * 
+     * @return The repository system session to forward calls to, never {@code null}.
+     */
+    protected abstract RepositorySystemSession getSession();
+
+    public boolean isOffline()
+    {
+        return getSession().isOffline();
+    }
+
+    public boolean isIgnoreArtifactDescriptorRepositories()
+    {
+        return getSession().isIgnoreArtifactDescriptorRepositories();
+    }
+
+    public ResolutionErrorPolicy getResolutionErrorPolicy()
+    {
+        return getSession().getResolutionErrorPolicy();
+    }
+
+    public ArtifactDescriptorPolicy getArtifactDescriptorPolicy()
+    {
+        return getSession().getArtifactDescriptorPolicy();
+    }
+
+    public String getChecksumPolicy()
+    {
+        return getSession().getChecksumPolicy();
+    }
+
+    public String getUpdatePolicy()
+    {
+        return getSession().getUpdatePolicy();
+    }
+
+    public LocalRepository getLocalRepository()
+    {
+        return getSession().getLocalRepository();
+    }
+
+    public LocalRepositoryManager getLocalRepositoryManager()
+    {
+        return getSession().getLocalRepositoryManager();
+    }
+
+    public WorkspaceReader getWorkspaceReader()
+    {
+        return getSession().getWorkspaceReader();
+    }
+
+    public RepositoryListener getRepositoryListener()
+    {
+        return getSession().getRepositoryListener();
+    }
+
+    public TransferListener getTransferListener()
+    {
+        return getSession().getTransferListener();
+    }
+
+    public Map<String, String> getSystemProperties()
+    {
+        return getSession().getSystemProperties();
+    }
+
+    public Map<String, String> getUserProperties()
+    {
+        return getSession().getUserProperties();
+    }
+
+    public Map<String, Object> getConfigProperties()
+    {
+        return getSession().getConfigProperties();
+    }
+
+    public MirrorSelector getMirrorSelector()
+    {
+        return getSession().getMirrorSelector();
+    }
+
+    public ProxySelector getProxySelector()
+    {
+        return getSession().getProxySelector();
+    }
+
+    public AuthenticationSelector getAuthenticationSelector()
+    {
+        return getSession().getAuthenticationSelector();
+    }
+
+    public ArtifactTypeRegistry getArtifactTypeRegistry()
+    {
+        return getSession().getArtifactTypeRegistry();
+    }
+
+    public DependencyTraverser getDependencyTraverser()
+    {
+        return getSession().getDependencyTraverser();
+    }
+
+    public DependencyManager getDependencyManager()
+    {
+        return getSession().getDependencyManager();
+    }
+
+    public DependencySelector getDependencySelector()
+    {
+        return getSession().getDependencySelector();
+    }
+
+    public VersionFilter getVersionFilter()
+    {
+        return getSession().getVersionFilter();
+    }
+
+    public DependencyGraphTransformer getDependencyGraphTransformer()
+    {
+        return getSession().getDependencyGraphTransformer();
+    }
+
+    public SessionData getData()
+    {
+        return getSession().getData();
+    }
+
+    public RepositoryCache getCache()
+    {
+        return getSession().getCache();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/AbstractRepositoryListener.java b/org.argeo.slc.repo/src/org/eclipse/aether/AbstractRepositoryListener.java
new file mode 100644 (file)
index 0000000..eaaffc1
--- /dev/null
@@ -0,0 +1,103 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether;
+
+/**
+ * A skeleton implementation for custom repository listeners. The callback methods in this class do nothing.
+ */
+public abstract class AbstractRepositoryListener
+    implements RepositoryListener
+{
+
+    /**
+     * Enables subclassing.
+     */
+    protected AbstractRepositoryListener()
+    {
+    }
+
+    public void artifactDeployed( RepositoryEvent event )
+    {
+    }
+
+    public void artifactDeploying( RepositoryEvent event )
+    {
+    }
+
+    public void artifactDescriptorInvalid( RepositoryEvent event )
+    {
+    }
+
+    public void artifactDescriptorMissing( RepositoryEvent event )
+    {
+    }
+
+    public void artifactDownloaded( RepositoryEvent event )
+    {
+    }
+
+    public void artifactDownloading( RepositoryEvent event )
+    {
+    }
+
+    public void artifactInstalled( RepositoryEvent event )
+    {
+    }
+
+    public void artifactInstalling( RepositoryEvent event )
+    {
+    }
+
+    public void artifactResolved( RepositoryEvent event )
+    {
+    }
+
+    public void artifactResolving( RepositoryEvent event )
+    {
+    }
+
+    public void metadataDeployed( RepositoryEvent event )
+    {
+    }
+
+    public void metadataDeploying( RepositoryEvent event )
+    {
+    }
+
+    public void metadataDownloaded( RepositoryEvent event )
+    {
+    }
+
+    public void metadataDownloading( RepositoryEvent event )
+    {
+    }
+
+    public void metadataInstalled( RepositoryEvent event )
+    {
+    }
+
+    public void metadataInstalling( RepositoryEvent event )
+    {
+    }
+
+    public void metadataInvalid( RepositoryEvent event )
+    {
+    }
+
+    public void metadataResolved( RepositoryEvent event )
+    {
+    }
+
+    public void metadataResolving( RepositoryEvent event )
+    {
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/ConfigurationProperties.java b/org.argeo.slc.repo/src/org/eclipse/aether/ConfigurationProperties.java
new file mode 100644 (file)
index 0000000..3cbd59c
--- /dev/null
@@ -0,0 +1,165 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether;
+
+/**
+ * The keys and defaults for common configuration properties.
+ * 
+ * @see RepositorySystemSession#getConfigProperties()
+ */
+public final class ConfigurationProperties
+{
+
+    private static final String PREFIX_AETHER = "aether.";
+
+    private static final String PREFIX_CONNECTOR = PREFIX_AETHER + "connector.";
+
+    /**
+     * The prefix for properties that control the priority of pluggable extensions like transporters. For example, for
+     * an extension with the fully qualified class name "org.eclipse.MyExtensionFactory", the configuration properties
+     * "aether.priority.org.eclipse.MyExtensionFactory", "aether.priority.MyExtensionFactory" and
+     * "aether.priority.MyExtension" will be consulted for the priority, in that order (obviously, the last key is only
+     * tried if the class name ends with "Factory"). The corresponding value is a float and the special value
+     * {@link Float#NaN} or "NaN" (case-sensitive) can be used to disable the extension.
+     */
+    public static final String PREFIX_PRIORITY = PREFIX_AETHER + "priority.";
+
+    /**
+     * A flag indicating whether the priorities of pluggable extensions are implicitly given by their iteration order
+     * such that the first extension has the highest priority. If set, an extension's built-in priority as well as any
+     * corresponding {@code aether.priority.*} configuration properties are ignored when searching for a suitable
+     * implementation among the available extensions. This priority mode is meant for cases where the application will
+     * present/inject extensions in the desired search order.
+     * 
+     * @see #DEFAULT_IMPLICIT_PRIORITIES
+     */
+    public static final String IMPLICIT_PRIORITIES = PREFIX_PRIORITY + "implicit";
+
+    /**
+     * The default extension priority mode if {@link #IMPLICIT_PRIORITIES} isn't set.
+     */
+    public static final boolean DEFAULT_IMPLICIT_PRIORITIES = false;
+
+    /**
+     * A flag indicating whether interaction with the user is allowed.
+     * 
+     * @see #DEFAULT_INTERACTIVE
+     */
+    public static final String INTERACTIVE = PREFIX_AETHER + "interactive";
+
+    /**
+     * The default interactive mode if {@link #INTERACTIVE} isn't set.
+     */
+    public static final boolean DEFAULT_INTERACTIVE = false;
+
+    /**
+     * The user agent that repository connectors should report to servers.
+     * 
+     * @see #DEFAULT_USER_AGENT
+     */
+    public static final String USER_AGENT = PREFIX_CONNECTOR + "userAgent";
+
+    /**
+     * The default user agent to use if {@link #USER_AGENT} isn't set.
+     */
+    public static final String DEFAULT_USER_AGENT = "Aether";
+
+    /**
+     * The maximum amount of time (in milliseconds) to wait for a successful connection to a remote server. Non-positive
+     * values indicate no timeout.
+     * 
+     * @see #DEFAULT_CONNECT_TIMEOUT
+     */
+    public static final String CONNECT_TIMEOUT = PREFIX_CONNECTOR + "connectTimeout";
+
+    /**
+     * The default connect timeout to use if {@link #CONNECT_TIMEOUT} isn't set.
+     */
+    public static final int DEFAULT_CONNECT_TIMEOUT = 10 * 1000;
+
+    /**
+     * The maximum amount of time (in milliseconds) to wait for remaining data to arrive from a remote server. Note that
+     * this timeout does not restrict the overall duration of a request, it only restricts the duration of inactivity
+     * between consecutive data packets. Non-positive values indicate no timeout.
+     * 
+     * @see #DEFAULT_REQUEST_TIMEOUT
+     */
+    public static final String REQUEST_TIMEOUT = PREFIX_CONNECTOR + "requestTimeout";
+
+    /**
+     * The default request timeout to use if {@link #REQUEST_TIMEOUT} isn't set.
+     */
+    public static final int DEFAULT_REQUEST_TIMEOUT = 1800 * 1000;
+
+    /**
+     * The request headers to use for HTTP-based repository connectors. The headers are specified using a
+     * {@code Map<String, String>}, mapping a header name to its value. Besides this general key, clients may also
+     * specify headers for a specific remote repository by appending the suffix {@code .<repoId>} to this key when
+     * storing the headers map. The repository-specific headers map is supposed to be complete, i.e. is not merged with
+     * the general headers map.
+     */
+    public static final String HTTP_HEADERS = PREFIX_CONNECTOR + "http.headers";
+
+    /**
+     * The encoding/charset to use when exchanging credentials with HTTP servers. Besides this general key, clients may
+     * also specify the encoding for a specific remote repository by appending the suffix {@code .<repoId>} to this key
+     * when storing the charset name.
+     * 
+     * @see #DEFAULT_HTTP_CREDENTIAL_ENCODING
+     */
+    public static final String HTTP_CREDENTIAL_ENCODING = PREFIX_CONNECTOR + "http.credentialEncoding";
+
+    /**
+     * The default encoding/charset to use if {@link #HTTP_CREDENTIAL_ENCODING} isn't set.
+     */
+    public static final String DEFAULT_HTTP_CREDENTIAL_ENCODING = "ISO-8859-1";
+
+    /**
+     * An option indicating whether authentication configured for a HTTP repository should also be used with any host
+     * that the original server might redirect requests to. Unless enabled, credentials are only exchanged with the
+     * original host from the repository URL and not supplied to different hosts encountered during redirects. The
+     * option value can either be a boolean flag or a comma-separated list of host names denoting the whitelist of
+     * original hosts whose redirects can be trusted and should use the configured authentication no matter the
+     * destination host(s). Alternatively, the suffix {@code .<repoId>} can be appended to this configuration key to
+     * control the behavior for a specific repository id.
+     * 
+     * @see #DEFAULT_HTTP_REDIRECTED_AUTHENTICATION
+     * @since 1.1.0
+     */
+    public static final String HTTP_REDIRECTED_AUTHENTICATION = PREFIX_CONNECTOR + "http.redirectedAuthentication";
+
+    /**
+     * The default handling of authentication during HTTP redirects if {@link #HTTP_REDIRECTED_AUTHENTICATION} isn't
+     * set.
+     * 
+     * @since 1.1.0
+     */
+    public static final String DEFAULT_HTTP_REDIRECTED_AUTHENTICATION = "false";
+
+    /**
+     * A flag indicating whether checksums which are retrieved during checksum validation should be persisted in the
+     * local filesystem next to the file they provide the checksum for.
+     * 
+     * @see #DEFAULT_PERSISTED_CHECKSUMS
+     */
+    public static final String PERSISTED_CHECKSUMS = PREFIX_CONNECTOR + "persistedChecksums";
+
+    /**
+     * The default checksum persistence mode if {@link #PERSISTED_CHECKSUMS} isn't set.
+     */
+    public static final boolean DEFAULT_PERSISTED_CHECKSUMS = true;
+
+    private ConfigurationProperties()
+    {
+        // hide constructor
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/DefaultRepositoryCache.java b/org.argeo.slc.repo/src/org/eclipse/aether/DefaultRepositoryCache.java
new file mode 100644 (file)
index 0000000..12d2789
--- /dev/null
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A simplistic repository cache backed by a thread-safe map. The simplistic nature of this cache makes it only suitable
+ * for use with short-lived repository system sessions where pruning of cache data is not required.
+ */
+public final class DefaultRepositoryCache
+    implements RepositoryCache
+{
+
+    private final Map<Object, Object> cache = new ConcurrentHashMap<Object, Object>( 256 );
+
+    public Object get( RepositorySystemSession session, Object key )
+    {
+        return cache.get( key );
+    }
+
+    public void put( RepositorySystemSession session, Object key, Object data )
+    {
+        if ( data != null )
+        {
+            cache.put( key, data );
+        }
+        else
+        {
+            cache.remove( key );
+        }
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/DefaultRepositorySystemSession.java b/org.argeo.slc.repo/src/org/eclipse/aether/DefaultRepositorySystemSession.java
new file mode 100644 (file)
index 0000000..363ede6
--- /dev/null
@@ -0,0 +1,825 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.aether.artifact.ArtifactType;
+import org.eclipse.aether.artifact.ArtifactTypeRegistry;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.collection.DependencyManager;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.collection.DependencyTraverser;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationSelector;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.MirrorSelector;
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.ProxySelector;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.repository.WorkspaceReader;
+import org.eclipse.aether.resolution.ArtifactDescriptorPolicy;
+import org.eclipse.aether.resolution.ResolutionErrorPolicy;
+import org.eclipse.aether.transfer.TransferListener;
+
+/**
+ * A simple repository system session.
+ * <p>
+ * <strong>Note:</strong> This class is not thread-safe. It is assumed that the mutators get only called during an
+ * initialization phase and that the session itself is not changed once initialized and being used by the repository
+ * system. It is recommended to call {@link #setReadOnly()} once the session has been fully initialized to prevent
+ * accidental manipulation of it afterwards.
+ */
+public final class DefaultRepositorySystemSession
+    implements RepositorySystemSession
+{
+
+    private boolean readOnly;
+
+    private boolean offline;
+
+    private boolean ignoreArtifactDescriptorRepositories;
+
+    private ResolutionErrorPolicy resolutionErrorPolicy;
+
+    private ArtifactDescriptorPolicy artifactDescriptorPolicy;
+
+    private String checksumPolicy;
+
+    private String updatePolicy;
+
+    private LocalRepositoryManager localRepositoryManager;
+
+    private WorkspaceReader workspaceReader;
+
+    private RepositoryListener repositoryListener;
+
+    private TransferListener transferListener;
+
+    private Map<String, String> systemProperties;
+
+    private Map<String, String> systemPropertiesView;
+
+    private Map<String, String> userProperties;
+
+    private Map<String, String> userPropertiesView;
+
+    private Map<String, Object> configProperties;
+
+    private Map<String, Object> configPropertiesView;
+
+    private MirrorSelector mirrorSelector;
+
+    private ProxySelector proxySelector;
+
+    private AuthenticationSelector authenticationSelector;
+
+    private ArtifactTypeRegistry artifactTypeRegistry;
+
+    private DependencyTraverser dependencyTraverser;
+
+    private DependencyManager dependencyManager;
+
+    private DependencySelector dependencySelector;
+
+    private VersionFilter versionFilter;
+
+    private DependencyGraphTransformer dependencyGraphTransformer;
+
+    private SessionData data;
+
+    private RepositoryCache cache;
+
+    /**
+     * Creates an uninitialized session. <em>Note:</em> The new session is not ready to use, as a bare minimum,
+     * {@link #setLocalRepositoryManager(LocalRepositoryManager)} needs to be called but usually other settings also
+     * need to be customized to achieve meaningful behavior.
+     */
+    public DefaultRepositorySystemSession()
+    {
+        systemProperties = new HashMap<String, String>();
+        systemPropertiesView = Collections.unmodifiableMap( systemProperties );
+        userProperties = new HashMap<String, String>();
+        userPropertiesView = Collections.unmodifiableMap( userProperties );
+        configProperties = new HashMap<String, Object>();
+        configPropertiesView = Collections.unmodifiableMap( configProperties );
+        mirrorSelector = NullMirrorSelector.INSTANCE;
+        proxySelector = NullProxySelector.INSTANCE;
+        authenticationSelector = NullAuthenticationSelector.INSTANCE;
+        artifactTypeRegistry = NullArtifactTypeRegistry.INSTANCE;
+        data = new DefaultSessionData();
+    }
+
+    /**
+     * Creates a shallow copy of the specified session. Actually, the copy is not completely shallow, all maps holding
+     * system/user/config properties are copied as well. In other words, invoking any mutator on the new session itself
+     * has no effect on the original session. Other mutable objects like the session data and cache (if any) are not
+     * copied and will be shared with the original session unless reconfigured.
+     * 
+     * @param session The session to copy, must not be {@code null}.
+     */
+    public DefaultRepositorySystemSession( RepositorySystemSession session )
+    {
+        if ( session == null )
+        {
+            throw new IllegalArgumentException( "repository system session not specified" );
+        }
+
+        setOffline( session.isOffline() );
+        setIgnoreArtifactDescriptorRepositories( session.isIgnoreArtifactDescriptorRepositories() );
+        setResolutionErrorPolicy( session.getResolutionErrorPolicy() );
+        setArtifactDescriptorPolicy( session.getArtifactDescriptorPolicy() );
+        setChecksumPolicy( session.getChecksumPolicy() );
+        setUpdatePolicy( session.getUpdatePolicy() );
+        setLocalRepositoryManager( session.getLocalRepositoryManager() );
+        setWorkspaceReader( session.getWorkspaceReader() );
+        setRepositoryListener( session.getRepositoryListener() );
+        setTransferListener( session.getTransferListener() );
+        setSystemProperties( session.getSystemProperties() );
+        setUserProperties( session.getUserProperties() );
+        setConfigProperties( session.getConfigProperties() );
+        setMirrorSelector( session.getMirrorSelector() );
+        setProxySelector( session.getProxySelector() );
+        setAuthenticationSelector( session.getAuthenticationSelector() );
+        setArtifactTypeRegistry( session.getArtifactTypeRegistry() );
+        setDependencyTraverser( session.getDependencyTraverser() );
+        setDependencyManager( session.getDependencyManager() );
+        setDependencySelector( session.getDependencySelector() );
+        setVersionFilter( session.getVersionFilter() );
+        setDependencyGraphTransformer( session.getDependencyGraphTransformer() );
+        setData( session.getData() );
+        setCache( session.getCache() );
+    }
+
+    public boolean isOffline()
+    {
+        return offline;
+    }
+
+    /**
+     * Controls whether the repository system operates in offline mode and avoids/refuses any access to remote
+     * repositories.
+     * 
+     * @param offline {@code true} if the repository system is in offline mode, {@code false} otherwise.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setOffline( boolean offline )
+    {
+        failIfReadOnly();
+        this.offline = offline;
+        return this;
+    }
+
+    public boolean isIgnoreArtifactDescriptorRepositories()
+    {
+        return ignoreArtifactDescriptorRepositories;
+    }
+
+    /**
+     * Controls whether repositories declared in artifact descriptors should be ignored during transitive dependency
+     * collection. If enabled, only the repositories originally provided with the collect request will be considered.
+     * 
+     * @param ignoreArtifactDescriptorRepositories {@code true} to ignore additional repositories from artifact
+     *            descriptors, {@code false} to merge those with the originally specified repositories.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setIgnoreArtifactDescriptorRepositories( boolean ignoreArtifactDescriptorRepositories )
+    {
+        failIfReadOnly();
+        this.ignoreArtifactDescriptorRepositories = ignoreArtifactDescriptorRepositories;
+        return this;
+    }
+
+    public ResolutionErrorPolicy getResolutionErrorPolicy()
+    {
+        return resolutionErrorPolicy;
+    }
+
+    /**
+     * Sets the policy which controls whether resolutions errors from remote repositories should be cached.
+     * 
+     * @param resolutionErrorPolicy The resolution error policy for this session, may be {@code null} if resolution
+     *            errors should generally not be cached.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setResolutionErrorPolicy( ResolutionErrorPolicy resolutionErrorPolicy )
+    {
+        failIfReadOnly();
+        this.resolutionErrorPolicy = resolutionErrorPolicy;
+        return this;
+    }
+
+    public ArtifactDescriptorPolicy getArtifactDescriptorPolicy()
+    {
+        return artifactDescriptorPolicy;
+    }
+
+    /**
+     * Sets the policy which controls how errors related to reading artifact descriptors should be handled.
+     * 
+     * @param artifactDescriptorPolicy The descriptor error policy for this session, may be {@code null} if descriptor
+     *            errors should generally not be tolerated.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setArtifactDescriptorPolicy( ArtifactDescriptorPolicy artifactDescriptorPolicy )
+    {
+        failIfReadOnly();
+        this.artifactDescriptorPolicy = artifactDescriptorPolicy;
+        return this;
+    }
+
+    public String getChecksumPolicy()
+    {
+        return checksumPolicy;
+    }
+
+    /**
+     * Sets the global checksum policy. If set, the global checksum policy overrides the checksum policies of the remote
+     * repositories being used for resolution.
+     * 
+     * @param checksumPolicy The global checksum policy, may be {@code null}/empty to apply the per-repository policies.
+     * @return This session for chaining, never {@code null}.
+     * @see RepositoryPolicy#CHECKSUM_POLICY_FAIL
+     * @see RepositoryPolicy#CHECKSUM_POLICY_IGNORE
+     * @see RepositoryPolicy#CHECKSUM_POLICY_WARN
+     */
+    public DefaultRepositorySystemSession setChecksumPolicy( String checksumPolicy )
+    {
+        failIfReadOnly();
+        this.checksumPolicy = checksumPolicy;
+        return this;
+    }
+
+    public String getUpdatePolicy()
+    {
+        return updatePolicy;
+    }
+
+    /**
+     * Sets the global update policy. If set, the global update policy overrides the update policies of the remote
+     * repositories being used for resolution.
+     * 
+     * @param updatePolicy The global update policy, may be {@code null}/empty to apply the per-repository policies.
+     * @return This session for chaining, never {@code null}.
+     * @see RepositoryPolicy#UPDATE_POLICY_ALWAYS
+     * @see RepositoryPolicy#UPDATE_POLICY_DAILY
+     * @see RepositoryPolicy#UPDATE_POLICY_NEVER
+     */
+    public DefaultRepositorySystemSession setUpdatePolicy( String updatePolicy )
+    {
+        failIfReadOnly();
+        this.updatePolicy = updatePolicy;
+        return this;
+    }
+
+    public LocalRepository getLocalRepository()
+    {
+        LocalRepositoryManager lrm = getLocalRepositoryManager();
+        return ( lrm != null ) ? lrm.getRepository() : null;
+    }
+
+    public LocalRepositoryManager getLocalRepositoryManager()
+    {
+        return localRepositoryManager;
+    }
+
+    /**
+     * Sets the local repository manager used during this session. <em>Note:</em> Eventually, a valid session must have
+     * a local repository manager set.
+     * 
+     * @param localRepositoryManager The local repository manager used during this session, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setLocalRepositoryManager( LocalRepositoryManager localRepositoryManager )
+    {
+        failIfReadOnly();
+        this.localRepositoryManager = localRepositoryManager;
+        return this;
+    }
+
+    public WorkspaceReader getWorkspaceReader()
+    {
+        return workspaceReader;
+    }
+
+    /**
+     * Sets the workspace reader used during this session. If set, the workspace reader will usually be consulted first
+     * to resolve artifacts.
+     * 
+     * @param workspaceReader The workspace reader for this session, may be {@code null} if none.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setWorkspaceReader( WorkspaceReader workspaceReader )
+    {
+        failIfReadOnly();
+        this.workspaceReader = workspaceReader;
+        return this;
+    }
+
+    public RepositoryListener getRepositoryListener()
+    {
+        return repositoryListener;
+    }
+
+    /**
+     * Sets the listener being notified of actions in the repository system.
+     * 
+     * @param repositoryListener The repository listener, may be {@code null} if none.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setRepositoryListener( RepositoryListener repositoryListener )
+    {
+        failIfReadOnly();
+        this.repositoryListener = repositoryListener;
+        return this;
+    }
+
+    public TransferListener getTransferListener()
+    {
+        return transferListener;
+    }
+
+    /**
+     * Sets the listener being notified of uploads/downloads by the repository system.
+     * 
+     * @param transferListener The transfer listener, may be {@code null} if none.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setTransferListener( TransferListener transferListener )
+    {
+        failIfReadOnly();
+        this.transferListener = transferListener;
+        return this;
+    }
+
+    private <T> Map<String, T> copySafe( Map<?, ?> table, Class<T> valueType )
+    {
+        Map<String, T> map;
+        if ( table == null || table.isEmpty() )
+        {
+            map = new HashMap<String, T>();
+        }
+        else
+        {
+            map = new HashMap<String, T>( (int) ( table.size() / 0.75f ) + 1 );
+            for ( Map.Entry<?, ?> entry : table.entrySet() )
+            {
+                Object key = entry.getKey();
+                if ( key instanceof String )
+                {
+                    Object value = entry.getValue();
+                    if ( valueType.isInstance( value ) )
+                    {
+                        map.put( key.toString(), valueType.cast( value ) );
+                    }
+                }
+            }
+        }
+        return map;
+    }
+
+    public Map<String, String> getSystemProperties()
+    {
+        return systemPropertiesView;
+    }
+
+    /**
+     * Sets the system properties to use, e.g. for processing of artifact descriptors. System properties are usually
+     * collected from the runtime environment like {@link System#getProperties()} and environment variables.
+     * <p>
+     * <em>Note:</em> System properties are of type {@code Map<String, String>} and any key-value pair in the input map
+     * that doesn't match this type will be silently ignored.
+     * 
+     * @param systemProperties The system properties, may be {@code null} or empty if none.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setSystemProperties( Map<?, ?> systemProperties )
+    {
+        failIfReadOnly();
+        this.systemProperties = copySafe( systemProperties, String.class );
+        systemPropertiesView = Collections.unmodifiableMap( this.systemProperties );
+        return this;
+    }
+
+    /**
+     * Sets the specified system property.
+     * 
+     * @param key The property key, must not be {@code null}.
+     * @param value The property value, may be {@code null} to remove/unset the property.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setSystemProperty( String key, String value )
+    {
+        failIfReadOnly();
+        if ( value != null )
+        {
+            systemProperties.put( key, value );
+        }
+        else
+        {
+            systemProperties.remove( key );
+        }
+        return this;
+    }
+
+    public Map<String, String> getUserProperties()
+    {
+        return userPropertiesView;
+    }
+
+    /**
+     * Sets the user properties to use, e.g. for processing of artifact descriptors. User properties are similar to
+     * system properties but are set on the discretion of the user and hence are considered of higher priority than
+     * system properties in case of conflicts.
+     * <p>
+     * <em>Note:</em> User properties are of type {@code Map<String, String>} and any key-value pair in the input map
+     * that doesn't match this type will be silently ignored.
+     * 
+     * @param userProperties The user properties, may be {@code null} or empty if none.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setUserProperties( Map<?, ?> userProperties )
+    {
+        failIfReadOnly();
+        this.userProperties = copySafe( userProperties, String.class );
+        userPropertiesView = Collections.unmodifiableMap( this.userProperties );
+        return this;
+    }
+
+    /**
+     * Sets the specified user property.
+     * 
+     * @param key The property key, must not be {@code null}.
+     * @param value The property value, may be {@code null} to remove/unset the property.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setUserProperty( String key, String value )
+    {
+        failIfReadOnly();
+        if ( value != null )
+        {
+            userProperties.put( key, value );
+        }
+        else
+        {
+            userProperties.remove( key );
+        }
+        return this;
+    }
+
+    public Map<String, Object> getConfigProperties()
+    {
+        return configPropertiesView;
+    }
+
+    /**
+     * Sets the configuration properties used to tweak internal aspects of the repository system (e.g. thread pooling,
+     * connector-specific behavior, etc.).
+     * <p>
+     * <em>Note:</em> Configuration properties are of type {@code Map<String, Object>} and any key-value pair in the
+     * input map that doesn't match this type will be silently ignored.
+     * 
+     * @param configProperties The configuration properties, may be {@code null} or empty if none.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setConfigProperties( Map<?, ?> configProperties )
+    {
+        failIfReadOnly();
+        this.configProperties = copySafe( configProperties, Object.class );
+        configPropertiesView = Collections.unmodifiableMap( this.configProperties );
+        return this;
+    }
+
+    /**
+     * Sets the specified configuration property.
+     * 
+     * @param key The property key, must not be {@code null}.
+     * @param value The property value, may be {@code null} to remove/unset the property.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setConfigProperty( String key, Object value )
+    {
+        failIfReadOnly();
+        if ( value != null )
+        {
+            configProperties.put( key, value );
+        }
+        else
+        {
+            configProperties.remove( key );
+        }
+        return this;
+    }
+
+    public MirrorSelector getMirrorSelector()
+    {
+        return mirrorSelector;
+    }
+
+    /**
+     * Sets the mirror selector to use for repositories discovered in artifact descriptors. Note that this selector is
+     * not used for remote repositories which are passed as request parameters to the repository system, those
+     * repositories are supposed to denote the effective repositories.
+     * 
+     * @param mirrorSelector The mirror selector to use, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setMirrorSelector( MirrorSelector mirrorSelector )
+    {
+        failIfReadOnly();
+        this.mirrorSelector = mirrorSelector;
+        if ( this.mirrorSelector == null )
+        {
+            this.mirrorSelector = NullMirrorSelector.INSTANCE;
+        }
+        return this;
+    }
+
+    public ProxySelector getProxySelector()
+    {
+        return proxySelector;
+    }
+
+    /**
+     * Sets the proxy selector to use for repositories discovered in artifact descriptors. Note that this selector is
+     * not used for remote repositories which are passed as request parameters to the repository system, those
+     * repositories are supposed to have their proxy (if any) already set.
+     * 
+     * @param proxySelector The proxy selector to use, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     * @see org.eclipse.aether.repository.RemoteRepository#getProxy()
+     */
+    public DefaultRepositorySystemSession setProxySelector( ProxySelector proxySelector )
+    {
+        failIfReadOnly();
+        this.proxySelector = proxySelector;
+        if ( this.proxySelector == null )
+        {
+            this.proxySelector = NullProxySelector.INSTANCE;
+        }
+        return this;
+    }
+
+    public AuthenticationSelector getAuthenticationSelector()
+    {
+        return authenticationSelector;
+    }
+
+    /**
+     * Sets the authentication selector to use for repositories discovered in artifact descriptors. Note that this
+     * selector is not used for remote repositories which are passed as request parameters to the repository system,
+     * those repositories are supposed to have their authentication (if any) already set.
+     * 
+     * @param authenticationSelector The authentication selector to use, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     * @see org.eclipse.aether.repository.RemoteRepository#getAuthentication()
+     */
+    public DefaultRepositorySystemSession setAuthenticationSelector( AuthenticationSelector authenticationSelector )
+    {
+        failIfReadOnly();
+        this.authenticationSelector = authenticationSelector;
+        if ( this.authenticationSelector == null )
+        {
+            this.authenticationSelector = NullAuthenticationSelector.INSTANCE;
+        }
+        return this;
+    }
+
+    public ArtifactTypeRegistry getArtifactTypeRegistry()
+    {
+        return artifactTypeRegistry;
+    }
+
+    /**
+     * Sets the registry of artifact types recognized by this session.
+     * 
+     * @param artifactTypeRegistry The artifact type registry, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setArtifactTypeRegistry( ArtifactTypeRegistry artifactTypeRegistry )
+    {
+        failIfReadOnly();
+        this.artifactTypeRegistry = artifactTypeRegistry;
+        if ( this.artifactTypeRegistry == null )
+        {
+            this.artifactTypeRegistry = NullArtifactTypeRegistry.INSTANCE;
+        }
+        return this;
+    }
+
+    public DependencyTraverser getDependencyTraverser()
+    {
+        return dependencyTraverser;
+    }
+
+    /**
+     * Sets the dependency traverser to use for building dependency graphs.
+     * 
+     * @param dependencyTraverser The dependency traverser to use for building dependency graphs, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setDependencyTraverser( DependencyTraverser dependencyTraverser )
+    {
+        failIfReadOnly();
+        this.dependencyTraverser = dependencyTraverser;
+        return this;
+    }
+
+    public DependencyManager getDependencyManager()
+    {
+        return dependencyManager;
+    }
+
+    /**
+     * Sets the dependency manager to use for building dependency graphs.
+     * 
+     * @param dependencyManager The dependency manager to use for building dependency graphs, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setDependencyManager( DependencyManager dependencyManager )
+    {
+        failIfReadOnly();
+        this.dependencyManager = dependencyManager;
+        return this;
+    }
+
+    public DependencySelector getDependencySelector()
+    {
+        return dependencySelector;
+    }
+
+    /**
+     * Sets the dependency selector to use for building dependency graphs.
+     * 
+     * @param dependencySelector The dependency selector to use for building dependency graphs, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setDependencySelector( DependencySelector dependencySelector )
+    {
+        failIfReadOnly();
+        this.dependencySelector = dependencySelector;
+        return this;
+    }
+
+    public VersionFilter getVersionFilter()
+    {
+        return versionFilter;
+    }
+
+    /**
+     * Sets the version filter to use for building dependency graphs.
+     * 
+     * @param versionFilter The version filter to use for building dependency graphs, may be {@code null} to not filter
+     *            versions.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setVersionFilter( VersionFilter versionFilter )
+    {
+        failIfReadOnly();
+        this.versionFilter = versionFilter;
+        return this;
+    }
+
+    public DependencyGraphTransformer getDependencyGraphTransformer()
+    {
+        return dependencyGraphTransformer;
+    }
+
+    /**
+     * Sets the dependency graph transformer to use for building dependency graphs.
+     * 
+     * @param dependencyGraphTransformer The dependency graph transformer to use for building dependency graphs, may be
+     *            {@code null}.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setDependencyGraphTransformer( DependencyGraphTransformer dependencyGraphTransformer )
+    {
+        failIfReadOnly();
+        this.dependencyGraphTransformer = dependencyGraphTransformer;
+        return this;
+    }
+
+    public SessionData getData()
+    {
+        return data;
+    }
+
+    /**
+     * Sets the custom data associated with this session.
+     * 
+     * @param data The session data, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setData( SessionData data )
+    {
+        failIfReadOnly();
+        this.data = data;
+        if ( this.data == null )
+        {
+            this.data = new DefaultSessionData();
+        }
+        return this;
+    }
+
+    public RepositoryCache getCache()
+    {
+        return cache;
+    }
+
+    /**
+     * Sets the cache the repository system may use to save data for future reuse during the session.
+     * 
+     * @param cache The repository cache, may be {@code null} if none.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setCache( RepositoryCache cache )
+    {
+        failIfReadOnly();
+        this.cache = cache;
+        return this;
+    }
+
+    /**
+     * Marks this session as read-only such that any future attempts to call its mutators will fail with an exception.
+     * Marking an already read-only session as read-only has no effect. The session's data and cache remain writable
+     * though.
+     */
+    public void setReadOnly()
+    {
+        readOnly = true;
+    }
+
+    private void failIfReadOnly()
+    {
+        if ( readOnly )
+        {
+            throw new IllegalStateException( "repository system session is read-only" );
+        }
+    }
+
+    static class NullProxySelector
+        implements ProxySelector
+    {
+
+        public static final ProxySelector INSTANCE = new NullProxySelector();
+
+        public Proxy getProxy( RemoteRepository repository )
+        {
+            return repository.getProxy();
+        }
+
+    }
+
+    static class NullMirrorSelector
+        implements MirrorSelector
+    {
+
+        public static final MirrorSelector INSTANCE = new NullMirrorSelector();
+
+        public RemoteRepository getMirror( RemoteRepository repository )
+        {
+            return null;
+        }
+
+    }
+
+    static class NullAuthenticationSelector
+        implements AuthenticationSelector
+    {
+
+        public static final AuthenticationSelector INSTANCE = new NullAuthenticationSelector();
+
+        public Authentication getAuthentication( RemoteRepository repository )
+        {
+            return repository.getAuthentication();
+        }
+
+    }
+
+    static final class NullArtifactTypeRegistry
+        implements ArtifactTypeRegistry
+    {
+
+        public static final ArtifactTypeRegistry INSTANCE = new NullArtifactTypeRegistry();
+
+        public ArtifactType get( String typeId )
+        {
+            return null;
+        }
+
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/DefaultSessionData.java b/org.argeo.slc.repo/src/org/eclipse/aether/DefaultSessionData.java
new file mode 100644 (file)
index 0000000..738cebc
--- /dev/null
@@ -0,0 +1,82 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * A simple session data storage backed by a thread-safe map.
+ */
+public final class DefaultSessionData
+    implements SessionData
+{
+
+    private final ConcurrentMap<Object, Object> data;
+
+    public DefaultSessionData()
+    {
+        data = new ConcurrentHashMap<Object, Object>();
+    }
+
+    public void set( Object key, Object value )
+    {
+        if ( key == null )
+        {
+            throw new IllegalArgumentException( "key must not be null" );
+        }
+
+        if ( value != null )
+        {
+            data.put( key, value );
+        }
+        else
+        {
+            data.remove( key );
+        }
+    }
+
+    public boolean set( Object key, Object oldValue, Object newValue )
+    {
+        if ( key == null )
+        {
+            throw new IllegalArgumentException( "key must not be null" );
+        }
+
+        if ( newValue != null )
+        {
+            if ( oldValue == null )
+            {
+                return data.putIfAbsent( key, newValue ) == null;
+            }
+            return data.replace( key, oldValue, newValue );
+        }
+        else
+        {
+            if ( oldValue == null )
+            {
+                return !data.containsKey( key );
+            }
+            return data.remove( key, oldValue );
+        }
+    }
+
+    public Object get( Object key )
+    {
+        if ( key == null )
+        {
+            throw new IllegalArgumentException( "key must not be null" );
+        }
+
+        return data.get( key );
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/RepositoryCache.java b/org.argeo.slc.repo/src/org/eclipse/aether/RepositoryCache.java
new file mode 100644 (file)
index 0000000..7363844
--- /dev/null
@@ -0,0 +1,50 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether;
+
+/**
+ * Caches auxiliary data used during repository access like already processed metadata. The data in the cache is meant
+ * for exclusive consumption by the repository system and is opaque to the cache implementation. <strong>Note:</strong>
+ * Actual cache implementations must be thread-safe.
+ * 
+ * @see RepositorySystemSession#getCache()
+ */
+public interface RepositoryCache
+{
+
+    /**
+     * Puts the specified data into the cache. It is entirely up to the cache implementation how long this data will be
+     * kept before being purged, i.e. callers must not make any assumptions about the lifetime of cached data.
+     * <p>
+     * <em>Warning:</em> The cache will directly save the provided reference. If the cached data is mutable, i.e. could
+     * be modified after being put into the cache, the caller is responsible for creating a copy of the original data
+     * and store the copy in the cache.
+     * 
+     * @param session The repository session during which the cache is accessed, must not be {@code null}.
+     * @param key The key to use for lookup of the data, must not be {@code null}.
+     * @param data The data to store in the cache, may be {@code null}.
+     */
+    void put( RepositorySystemSession session, Object key, Object data );
+
+    /**
+     * Gets the specified data from the cache.
+     * <p>
+     * <em>Warning:</em> The cache will directly return the saved reference. If the cached data is to be modified after
+     * its retrieval, the caller is responsible to create a copy of the returned data and use this instead of the cache
+     * record.
+     * 
+     * @param session The repository session during which the cache is accessed, must not be {@code null}.
+     * @param key The key to use for lookup of the data, must not be {@code null}.
+     * @return The requested data or {@code null} if none was present in the cache.
+     */
+    Object get( RepositorySystemSession session, Object key );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/RepositoryEvent.java b/org.argeo.slc.repo/src/org/eclipse/aether/RepositoryEvent.java
new file mode 100644 (file)
index 0000000..2abd800
--- /dev/null
@@ -0,0 +1,433 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.ArtifactRepository;
+
+/**
+ * An event describing an action performed by the repository system. Note that events which indicate the end of an
+ * action like {@link EventType#ARTIFACT_RESOLVED} are generally fired in both the success and the failure case. Use
+ * {@link #getException()} to check whether an event denotes success or failure.
+ * 
+ * @see RepositoryListener
+ * @see RepositoryEvent.Builder
+ */
+public final class RepositoryEvent
+{
+
+    /**
+     * The type of the repository event.
+     */
+    public enum EventType
+    {
+
+        /**
+         * @see RepositoryListener#artifactDescriptorInvalid(RepositoryEvent)
+         */
+        ARTIFACT_DESCRIPTOR_INVALID,
+
+        /**
+         * @see RepositoryListener#artifactDescriptorMissing(RepositoryEvent)
+         */
+        ARTIFACT_DESCRIPTOR_MISSING,
+
+        /**
+         * @see RepositoryListener#metadataInvalid(RepositoryEvent)
+         */
+        METADATA_INVALID,
+
+        /**
+         * @see RepositoryListener#artifactResolving(RepositoryEvent)
+         */
+        ARTIFACT_RESOLVING,
+
+        /**
+         * @see RepositoryListener#artifactResolved(RepositoryEvent)
+         */
+        ARTIFACT_RESOLVED,
+
+        /**
+         * @see RepositoryListener#metadataResolving(RepositoryEvent)
+         */
+        METADATA_RESOLVING,
+
+        /**
+         * @see RepositoryListener#metadataResolved(RepositoryEvent)
+         */
+        METADATA_RESOLVED,
+
+        /**
+         * @see RepositoryListener#artifactDownloading(RepositoryEvent)
+         */
+        ARTIFACT_DOWNLOADING,
+
+        /**
+         * @see RepositoryListener#artifactDownloaded(RepositoryEvent)
+         */
+        ARTIFACT_DOWNLOADED,
+
+        /**
+         * @see RepositoryListener#metadataDownloading(RepositoryEvent)
+         */
+        METADATA_DOWNLOADING,
+
+        /**
+         * @see RepositoryListener#metadataDownloaded(RepositoryEvent)
+         */
+        METADATA_DOWNLOADED,
+
+        /**
+         * @see RepositoryListener#artifactInstalling(RepositoryEvent)
+         */
+        ARTIFACT_INSTALLING,
+
+        /**
+         * @see RepositoryListener#artifactInstalled(RepositoryEvent)
+         */
+        ARTIFACT_INSTALLED,
+
+        /**
+         * @see RepositoryListener#metadataInstalling(RepositoryEvent)
+         */
+        METADATA_INSTALLING,
+
+        /**
+         * @see RepositoryListener#metadataInstalled(RepositoryEvent)
+         */
+        METADATA_INSTALLED,
+
+        /**
+         * @see RepositoryListener#artifactDeploying(RepositoryEvent)
+         */
+        ARTIFACT_DEPLOYING,
+
+        /**
+         * @see RepositoryListener#artifactDeployed(RepositoryEvent)
+         */
+        ARTIFACT_DEPLOYED,
+
+        /**
+         * @see RepositoryListener#metadataDeploying(RepositoryEvent)
+         */
+        METADATA_DEPLOYING,
+
+        /**
+         * @see RepositoryListener#metadataDeployed(RepositoryEvent)
+         */
+        METADATA_DEPLOYED
+
+    }
+
+    private final EventType type;
+
+    private final RepositorySystemSession session;
+
+    private final Artifact artifact;
+
+    private final Metadata metadata;
+
+    private final ArtifactRepository repository;
+
+    private final File file;
+
+    private final List<Exception> exceptions;
+
+    private final RequestTrace trace;
+
+    RepositoryEvent( Builder builder )
+    {
+        type = builder.type;
+        session = builder.session;
+        artifact = builder.artifact;
+        metadata = builder.metadata;
+        repository = builder.repository;
+        file = builder.file;
+        exceptions = builder.exceptions;
+        trace = builder.trace;
+    }
+
+    /**
+     * Gets the type of the event.
+     * 
+     * @return The type of the event, never {@code null}.
+     */
+    public EventType getType()
+    {
+        return type;
+    }
+
+    /**
+     * Gets the repository system session during which the event occurred.
+     * 
+     * @return The repository system session during which the event occurred, never {@code null}.
+     */
+    public RepositorySystemSession getSession()
+    {
+        return session;
+    }
+
+    /**
+     * Gets the artifact involved in the event (if any).
+     * 
+     * @return The involved artifact or {@code null} if none.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Gets the metadata involved in the event (if any).
+     * 
+     * @return The involved metadata or {@code null} if none.
+     */
+    public Metadata getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Gets the file involved in the event (if any).
+     * 
+     * @return The involved file or {@code null} if none.
+     */
+    public File getFile()
+    {
+        return file;
+    }
+
+    /**
+     * Gets the repository involved in the event (if any).
+     * 
+     * @return The involved repository or {@code null} if none.
+     */
+    public ArtifactRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Gets the exception that caused the event (if any). As a rule of thumb, an event accompanied by an exception
+     * indicates a failure of the corresponding action. If multiple exceptions occurred, this method returns the first
+     * exception.
+     * 
+     * @return The exception or {@code null} if none.
+     */
+    public Exception getException()
+    {
+        return exceptions.isEmpty() ? null : exceptions.get( 0 );
+    }
+
+    /**
+     * Gets the exceptions that caused the event (if any). As a rule of thumb, an event accompanied by exceptions
+     * indicates a failure of the corresponding action.
+     * 
+     * @return The exceptions, never {@code null}.
+     */
+    public List<Exception> getExceptions()
+    {
+        return exceptions;
+    }
+
+    /**
+     * Gets the trace information about the request during which the event occurred.
+     * 
+     * @return The trace information or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+        buffer.append( getType() );
+        if ( getArtifact() != null )
+        {
+            buffer.append( " " ).append( getArtifact() );
+        }
+        if ( getMetadata() != null )
+        {
+            buffer.append( " " ).append( getMetadata() );
+        }
+        if ( getFile() != null )
+        {
+            buffer.append( " (" ).append( getFile() ).append( ")" );
+        }
+        if ( getRepository() != null )
+        {
+            buffer.append( " @ " ).append( getRepository() );
+        }
+        return buffer.toString();
+    }
+
+    /**
+     * A builder to create events.
+     */
+    public static final class Builder
+    {
+
+        EventType type;
+
+        RepositorySystemSession session;
+
+        Artifact artifact;
+
+        Metadata metadata;
+
+        ArtifactRepository repository;
+
+        File file;
+
+        List<Exception> exceptions = Collections.emptyList();
+
+        RequestTrace trace;
+
+        /**
+         * Creates a new event builder for the specified session and event type.
+         * 
+         * @param session The repository system session, must not be {@code null}.
+         * @param type The type of the event, must not be {@code null}.
+         */
+        public Builder( RepositorySystemSession session, EventType type )
+        {
+            if ( session == null )
+            {
+                throw new IllegalArgumentException( "session not specified" );
+            }
+            this.session = session;
+            if ( type == null )
+            {
+                throw new IllegalArgumentException( "event type not specified" );
+            }
+            this.type = type;
+        }
+
+        /**
+         * Sets the artifact involved in the event.
+         * 
+         * @param artifact The involved artifact, may be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setArtifact( Artifact artifact )
+        {
+            this.artifact = artifact;
+            return this;
+        }
+
+        /**
+         * Sets the metadata involved in the event.
+         * 
+         * @param metadata The involved metadata, may be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setMetadata( Metadata metadata )
+        {
+            this.metadata = metadata;
+            return this;
+        }
+
+        /**
+         * Sets the repository involved in the event.
+         * 
+         * @param repository The involved repository, may be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setRepository( ArtifactRepository repository )
+        {
+            this.repository = repository;
+            return this;
+        }
+
+        /**
+         * Sets the file involved in the event.
+         * 
+         * @param file The involved file, may be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setFile( File file )
+        {
+            this.file = file;
+            return this;
+        }
+
+        /**
+         * Sets the exception causing the event.
+         * 
+         * @param exception The exception causing the event, may be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setException( Exception exception )
+        {
+            if ( exception != null )
+            {
+                this.exceptions = Collections.singletonList( exception );
+            }
+            else
+            {
+                this.exceptions = Collections.emptyList();
+            }
+            return this;
+        }
+
+        /**
+         * Sets the exceptions causing the event.
+         * 
+         * @param exceptions The exceptions causing the event, may be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setExceptions( List<Exception> exceptions )
+        {
+            if ( exceptions != null )
+            {
+                this.exceptions = exceptions;
+            }
+            else
+            {
+                this.exceptions = Collections.emptyList();
+            }
+            return this;
+        }
+
+        /**
+         * Sets the trace information about the request during which the event occurred.
+         * 
+         * @param trace The trace information, may be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setTrace( RequestTrace trace )
+        {
+            this.trace = trace;
+            return this;
+        }
+
+        /**
+         * Builds a new event from the current values of this builder. The state of the builder itself remains
+         * unchanged.
+         * 
+         * @return The event, never {@code null}.
+         */
+        public RepositoryEvent build()
+        {
+            return new RepositoryEvent( this );
+        }
+
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/RepositoryException.java b/org.argeo.slc.repo/src/org/eclipse/aether/RepositoryException.java
new file mode 100644 (file)
index 0000000..35f0cfd
--- /dev/null
@@ -0,0 +1,63 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether;
+
+/**
+ * The base class for exceptions thrown by the repository system. <em>Note:</em> Unless otherwise noted, instances of
+ * this class and its subclasses will not persist fields carrying extended error information during serialization.
+ */
+public class RepositoryException
+    extends Exception
+{
+
+    /**
+     * Creates a new exception with the specified detail message.
+     * 
+     * @param message The detail message, may be {@code null}.
+     */
+    public RepositoryException( String message )
+    {
+        super( message );
+    }
+
+    /**
+     * Creates a new exception with the specified detail message and cause.
+     * 
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public RepositoryException( String message, Throwable cause )
+    {
+        super( message, cause );
+    }
+
+    /**
+     * @noreference This method is not intended to be used by clients.
+     * @param prefix A message prefix for the cause.
+     * @param cause The error cause.
+     * @return The error message for the cause.
+     */
+    protected static String getMessage( String prefix, Throwable cause )
+    {
+        String msg = "";
+        if ( cause != null )
+        {
+            msg = cause.getMessage();
+            if ( msg == null || msg.length() <= 0 )
+            {
+                msg = cause.getClass().getSimpleName();
+            }
+            msg = prefix + msg;
+        }
+        return msg;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/RepositoryListener.java b/org.argeo.slc.repo/src/org/eclipse/aether/RepositoryListener.java
new file mode 100644 (file)
index 0000000..5f83923
--- /dev/null
@@ -0,0 +1,213 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether;
+
+/**
+ * A listener being notified of events from the repository system. In general, the system sends events upon termination
+ * of an operation like {@link #artifactResolved(RepositoryEvent)} regardless whether it succeeded or failed so
+ * listeners need to inspect the event details carefully. Also, the listener may be called from an arbitrary thread.
+ * <em>Note:</em> Implementors are strongly advised to inherit from {@link AbstractRepositoryListener} instead of
+ * directly implementing this interface.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getRepositoryListener()
+ * @see org.eclipse.aether.transfer.TransferListener
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface RepositoryListener
+{
+
+    /**
+     * Notifies the listener of a syntactically or semantically invalid artifact descriptor.
+     * {@link RepositoryEvent#getArtifact()} indicates the artifact whose descriptor is invalid and
+     * {@link RepositoryEvent#getExceptions()} carries the encountered errors. Depending on the session's
+     * {@link org.eclipse.aether.resolution.ArtifactDescriptorPolicy}, the underlying repository operation might abort
+     * with an exception or ignore the invalid descriptor.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactDescriptorInvalid( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of a missing artifact descriptor. {@link RepositoryEvent#getArtifact()} indicates the
+     * artifact whose descriptor is missing. Depending on the session's
+     * {@link org.eclipse.aether.resolution.ArtifactDescriptorPolicy}, the underlying repository operation might abort
+     * with an exception or ignore the missing descriptor.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactDescriptorMissing( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of syntactically or semantically invalid metadata. {@link RepositoryEvent#getMetadata()}
+     * indicates the invalid metadata and {@link RepositoryEvent#getExceptions()} carries the encountered errors. The
+     * underlying repository operation might still succeed, depending on whether the metadata in question is actually
+     * needed to carry out the resolution process.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataInvalid( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of an artifact that is about to be resolved. {@link RepositoryEvent#getArtifact()} denotes
+     * the artifact in question. Unlike the {@link #artifactDownloading(RepositoryEvent)} event, this event is fired
+     * regardless whether the artifact already exists locally or not.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactResolving( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of an artifact whose resolution has been completed, either successfully or not.
+     * {@link RepositoryEvent#getArtifact()} denotes the artifact in question and
+     * {@link RepositoryEvent#getExceptions()} indicates whether the resolution succeeded or failed. Unlike the
+     * {@link #artifactDownloaded(RepositoryEvent)} event, this event is fired regardless whether the artifact already
+     * exists locally or not.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactResolved( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of some metadata that is about to be resolved. {@link RepositoryEvent#getMetadata()}
+     * denotes the metadata in question. Unlike the {@link #metadataDownloading(RepositoryEvent)} event, this event is
+     * fired regardless whether the metadata already exists locally or not.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataResolving( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of some metadata whose resolution has been completed, either successfully or not.
+     * {@link RepositoryEvent#getMetadata()} denotes the metadata in question and
+     * {@link RepositoryEvent#getExceptions()} indicates whether the resolution succeeded or failed. Unlike the
+     * {@link #metadataDownloaded(RepositoryEvent)} event, this event is fired regardless whether the metadata already
+     * exists locally or not.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataResolved( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of an artifact that is about to be downloaded from a remote repository.
+     * {@link RepositoryEvent#getArtifact()} denotes the artifact in question and
+     * {@link RepositoryEvent#getRepository()} the source repository. Unlike the
+     * {@link #artifactResolving(RepositoryEvent)} event, this event is only fired when the artifact does not already
+     * exist locally.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactDownloading( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of an artifact whose download has been completed, either successfully or not.
+     * {@link RepositoryEvent#getArtifact()} denotes the artifact in question and
+     * {@link RepositoryEvent#getExceptions()} indicates whether the download succeeded or failed. Unlike the
+     * {@link #artifactResolved(RepositoryEvent)} event, this event is only fired when the artifact does not already
+     * exist locally.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactDownloaded( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of some metadata that is about to be downloaded from a remote repository.
+     * {@link RepositoryEvent#getMetadata()} denotes the metadata in question and
+     * {@link RepositoryEvent#getRepository()} the source repository. Unlike the
+     * {@link #metadataResolving(RepositoryEvent)} event, this event is only fired when the metadata does not already
+     * exist locally.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataDownloading( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of some metadata whose download has been completed, either successfully or not.
+     * {@link RepositoryEvent#getMetadata()} denotes the metadata in question and
+     * {@link RepositoryEvent#getExceptions()} indicates whether the download succeeded or failed. Unlike the
+     * {@link #metadataResolved(RepositoryEvent)} event, this event is only fired when the metadata does not already
+     * exist locally.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataDownloaded( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of an artifact that is about to be installed to the local repository.
+     * {@link RepositoryEvent#getArtifact()} denotes the artifact in question.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactInstalling( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of an artifact whose installation to the local repository has been completed, either
+     * successfully or not. {@link RepositoryEvent#getArtifact()} denotes the artifact in question and
+     * {@link RepositoryEvent#getExceptions()} indicates whether the installation succeeded or failed.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactInstalled( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of some metadata that is about to be installed to the local repository.
+     * {@link RepositoryEvent#getMetadata()} denotes the metadata in question.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataInstalling( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of some metadata whose installation to the local repository has been completed, either
+     * successfully or not. {@link RepositoryEvent#getMetadata()} denotes the metadata in question and
+     * {@link RepositoryEvent#getExceptions()} indicates whether the installation succeeded or failed.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataInstalled( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of an artifact that is about to be uploaded to a remote repository.
+     * {@link RepositoryEvent#getArtifact()} denotes the artifact in question and
+     * {@link RepositoryEvent#getRepository()} the destination repository.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactDeploying( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of an artifact whose upload to a remote repository has been completed, either successfully
+     * or not. {@link RepositoryEvent#getArtifact()} denotes the artifact in question and
+     * {@link RepositoryEvent#getExceptions()} indicates whether the upload succeeded or failed.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactDeployed( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of some metadata that is about to be uploaded to a remote repository.
+     * {@link RepositoryEvent#getMetadata()} denotes the metadata in question and
+     * {@link RepositoryEvent#getRepository()} the destination repository.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataDeploying( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of some metadata whose upload to a remote repository has been completed, either
+     * successfully or not. {@link RepositoryEvent#getMetadata()} denotes the metadata in question and
+     * {@link RepositoryEvent#getExceptions()} indicates whether the upload succeeded or failed.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataDeployed( RepositoryEvent event );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/RepositorySystem.java b/org.argeo.slc.repo/src/org/eclipse/aether/RepositorySystem.java
new file mode 100644 (file)
index 0000000..debdb7d
--- /dev/null
@@ -0,0 +1,268 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.collection.CollectRequest;
+import org.eclipse.aether.collection.CollectResult;
+import org.eclipse.aether.collection.DependencyCollectionException;
+import org.eclipse.aether.deployment.DeployRequest;
+import org.eclipse.aether.deployment.DeployResult;
+import org.eclipse.aether.deployment.DeploymentException;
+import org.eclipse.aether.installation.InstallRequest;
+import org.eclipse.aether.installation.InstallResult;
+import org.eclipse.aether.installation.InstallationException;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
+import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
+import org.eclipse.aether.resolution.ArtifactRequest;
+import org.eclipse.aether.resolution.ArtifactResolutionException;
+import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.resolution.DependencyRequest;
+import org.eclipse.aether.resolution.DependencyResolutionException;
+import org.eclipse.aether.resolution.DependencyResult;
+import org.eclipse.aether.resolution.MetadataRequest;
+import org.eclipse.aether.resolution.MetadataResult;
+import org.eclipse.aether.resolution.VersionRangeRequest;
+import org.eclipse.aether.resolution.VersionRangeResolutionException;
+import org.eclipse.aether.resolution.VersionRangeResult;
+import org.eclipse.aether.resolution.VersionRequest;
+import org.eclipse.aether.resolution.VersionResolutionException;
+import org.eclipse.aether.resolution.VersionResult;
+
+/**
+ * The main entry point to the repository system and its functionality. Note that obtaining a concrete implementation of
+ * this interface (e.g. via dependency injection, service locator, etc.) is dependent on the application and its
+ * specific needs, please consult the online documentation for examples and directions on booting the system.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface RepositorySystem
+{
+
+    /**
+     * Expands a version range to a list of matching versions, in ascending order. For example, resolves "[3.8,4.0)" to
+     * "3.8", "3.8.1", "3.8.2". Note that the returned list of versions is only dependent on the configured repositories
+     * and their contents, the list is not processed by the {@link RepositorySystemSession#getVersionFilter() session's
+     * version filter}.
+     * <p>
+     * The supplied request may also refer to a single concrete version rather than a version range. In this case
+     * though, the result contains simply the (parsed) input version, regardless of the repositories and their contents.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The version range request, must not be {@code null}.
+     * @return The version range result, never {@code null}.
+     * @throws VersionRangeResolutionException If the requested range could not be parsed. Note that an empty range does
+     *             not raise an exception.
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    VersionRangeResult resolveVersionRange( RepositorySystemSession session, VersionRangeRequest request )
+        throws VersionRangeResolutionException;
+
+    /**
+     * Resolves an artifact's meta version (if any) to a concrete version. For example, resolves "1.0-SNAPSHOT" to
+     * "1.0-20090208.132618-23".
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The version request, must not be {@code null}.
+     * @return The version result, never {@code null}.
+     * @throws VersionResolutionException If the metaversion could not be resolved.
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    VersionResult resolveVersion( RepositorySystemSession session, VersionRequest request )
+        throws VersionResolutionException;
+
+    /**
+     * Gets information about an artifact like its direct dependencies and potential relocations.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The descriptor request, must not be {@code null}.
+     * @return The descriptor result, never {@code null}.
+     * @throws ArtifactDescriptorException If the artifact descriptor could not be read.
+     * @see RepositorySystemSession#getArtifactDescriptorPolicy()
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    ArtifactDescriptorResult readArtifactDescriptor( RepositorySystemSession session, ArtifactDescriptorRequest request )
+        throws ArtifactDescriptorException;
+
+    /**
+     * Collects the transitive dependencies of an artifact and builds a dependency graph. Note that this operation is
+     * only concerned about determining the coordinates of the transitive dependencies. To also resolve the actual
+     * artifact files, use {@link #resolveDependencies(RepositorySystemSession, DependencyRequest)}.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The collection request, must not be {@code null}.
+     * @return The collection result, never {@code null}.
+     * @throws DependencyCollectionException If the dependency tree could not be built.
+     * @see RepositorySystemSession#getDependencyTraverser()
+     * @see RepositorySystemSession#getDependencyManager()
+     * @see RepositorySystemSession#getDependencySelector()
+     * @see RepositorySystemSession#getVersionFilter()
+     * @see RepositorySystemSession#getDependencyGraphTransformer()
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    CollectResult collectDependencies( RepositorySystemSession session, CollectRequest request )
+        throws DependencyCollectionException;
+
+    /**
+     * Collects and resolves the transitive dependencies of an artifact. This operation is essentially a combination of
+     * {@link #collectDependencies(RepositorySystemSession, CollectRequest)} and
+     * {@link #resolveArtifacts(RepositorySystemSession, Collection)}.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The dependency request, must not be {@code null}.
+     * @return The dependency result, never {@code null}.
+     * @throws DependencyResolutionException If the dependency tree could not be built or any dependency artifact could
+     *             not be resolved.
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    DependencyResult resolveDependencies( RepositorySystemSession session, DependencyRequest request )
+        throws DependencyResolutionException;
+
+    /**
+     * Resolves the path for an artifact. The artifact will be downloaded to the local repository if necessary. An
+     * artifact that is already resolved will be skipped and is not re-resolved. In general, callers must not assume any
+     * relationship between an artifact's resolved filename and its coordinates. Note that this method assumes that any
+     * relocations have already been processed.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The resolution request, must not be {@code null}.
+     * @return The resolution result, never {@code null}.
+     * @throws ArtifactResolutionException If the artifact could not be resolved.
+     * @see Artifact#getFile()
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    ArtifactResult resolveArtifact( RepositorySystemSession session, ArtifactRequest request )
+        throws ArtifactResolutionException;
+
+    /**
+     * Resolves the paths for a collection of artifacts. Artifacts will be downloaded to the local repository if
+     * necessary. Artifacts that are already resolved will be skipped and are not re-resolved. In general, callers must
+     * not assume any relationship between an artifact's filename and its coordinates. Note that this method assumes
+     * that any relocations have already been processed.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param requests The resolution requests, must not be {@code null}.
+     * @return The resolution results (in request order), never {@code null}.
+     * @throws ArtifactResolutionException If any artifact could not be resolved.
+     * @see Artifact#getFile()
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    List<ArtifactResult> resolveArtifacts( RepositorySystemSession session,
+                                           Collection<? extends ArtifactRequest> requests )
+        throws ArtifactResolutionException;
+
+    /**
+     * Resolves the paths for a collection of metadata. Metadata will be downloaded to the local repository if
+     * necessary, e.g. because it hasn't been cached yet or the cache is deemed outdated.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param requests The resolution requests, must not be {@code null}.
+     * @return The resolution results (in request order), never {@code null}.
+     * @see Metadata#getFile()
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    List<MetadataResult> resolveMetadata( RepositorySystemSession session,
+                                          Collection<? extends MetadataRequest> requests );
+
+    /**
+     * Installs a collection of artifacts and their accompanying metadata to the local repository.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The installation request, must not be {@code null}.
+     * @return The installation result, never {@code null}.
+     * @throws InstallationException If any artifact/metadata from the request could not be installed.
+     */
+    InstallResult install( RepositorySystemSession session, InstallRequest request )
+        throws InstallationException;
+
+    /**
+     * Uploads a collection of artifacts and their accompanying metadata to a remote repository.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The deployment request, must not be {@code null}.
+     * @return The deployment result, never {@code null}.
+     * @throws DeploymentException If any artifact/metadata from the request could not be deployed.
+     * @see #newDeploymentRepository(RepositorySystemSession, RemoteRepository)
+     */
+    DeployResult deploy( RepositorySystemSession session, DeployRequest request )
+        throws DeploymentException;
+
+    /**
+     * Creates a new manager for the specified local repository. If the specified local repository has no type, the
+     * default local repository type of the system will be used. <em>Note:</em> It is expected that this method
+     * invocation is one of the last steps of setting up a new session, in particular any configuration properties
+     * should have been set already.
+     * 
+     * @param session The repository system session from which to configure the manager, must not be {@code null}.
+     * @param localRepository The local repository to create a manager for, must not be {@code null}.
+     * @return The local repository manager, never {@code null}.
+     * @throws IllegalArgumentException If the specified repository type is not recognized or no base directory is
+     *             given.
+     */
+    LocalRepositoryManager newLocalRepositoryManager( RepositorySystemSession session, LocalRepository localRepository );
+
+    /**
+     * Creates a new synchronization context.
+     * 
+     * @param session The repository session during which the context will be used, must not be {@code null}.
+     * @param shared A flag indicating whether access to the artifacts/metadata associated with the new context can be
+     *            shared among concurrent readers or whether access needs to be exclusive to the calling thread.
+     * @return The synchronization context, never {@code null}.
+     */
+    SyncContext newSyncContext( RepositorySystemSession session, boolean shared );
+
+    /**
+     * Forms remote repositories suitable for artifact resolution by applying the session's authentication selector and
+     * similar network configuration to the given repository prototypes. As noted for
+     * {@link RepositorySystemSession#getAuthenticationSelector()} etc. the remote repositories passed to e.g.
+     * {@link #resolveArtifact(RepositorySystemSession, ArtifactRequest) resolveArtifact()} are used as is and expected
+     * to already carry any required authentication or proxy configuration. This method can be used to apply the
+     * authentication/proxy configuration from a session to a bare repository definition to obtain the complete
+     * repository definition for use in the resolution request.
+     * 
+     * @param session The repository system session from which to configure the repositories, must not be {@code null}.
+     * @param repositories The repository prototypes from which to derive the resolution repositories, must not be
+     *            {@code null} or contain {@code null} elements.
+     * @return The resolution repositories, never {@code null}. Note that there is generally no 1:1 relationship of the
+     *         obtained repositories to the original inputs due to mirror selection potentially aggregating multiple
+     *         repositories.
+     * @see #newDeploymentRepository(RepositorySystemSession, RemoteRepository)
+     */
+    List<RemoteRepository> newResolutionRepositories( RepositorySystemSession session,
+                                                      List<RemoteRepository> repositories );
+
+    /**
+     * Forms a remote repository suitable for artifact deployment by applying the session's authentication selector and
+     * similar network configuration to the given repository prototype. As noted for
+     * {@link RepositorySystemSession#getAuthenticationSelector()} etc. the remote repository passed to
+     * {@link #deploy(RepositorySystemSession, DeployRequest) deploy()} is used as is and expected to already carry any
+     * required authentication or proxy configuration. This method can be used to apply the authentication/proxy
+     * configuration from a session to a bare repository definition to obtain the complete repository definition for use
+     * in the deploy request.
+     * 
+     * @param session The repository system session from which to configure the repository, must not be {@code null}.
+     * @param repository The repository prototype from which to derive the deployment repository, must not be
+     *            {@code null}.
+     * @return The deployment repository, never {@code null}.
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    RemoteRepository newDeploymentRepository( RepositorySystemSession session, RemoteRepository repository );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/RepositorySystemSession.java b/org.argeo.slc.repo/src/org/eclipse/aether/RepositorySystemSession.java
new file mode 100644 (file)
index 0000000..96f51c1
--- /dev/null
@@ -0,0 +1,254 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether;
+
+import java.util.Map;
+
+import org.eclipse.aether.artifact.ArtifactTypeRegistry;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.collection.DependencyManager;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.collection.DependencyTraverser;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.repository.AuthenticationSelector;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.MirrorSelector;
+import org.eclipse.aether.repository.ProxySelector;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.repository.WorkspaceReader;
+import org.eclipse.aether.resolution.ArtifactDescriptorPolicy;
+import org.eclipse.aether.resolution.ResolutionErrorPolicy;
+import org.eclipse.aether.transfer.TransferListener;
+
+/**
+ * Defines settings and components that control the repository system. Once initialized, the session object itself is
+ * supposed to be immutable and hence can safely be shared across an entire application and any concurrent threads
+ * reading it. Components that wish to tweak some aspects of an existing session should use the copy constructor of
+ * {@link DefaultRepositorySystemSession} and its mutators to derive a custom session.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface RepositorySystemSession
+{
+
+    /**
+     * Indicates whether the repository system operates in offline mode and avoids/refuses any access to remote
+     * repositories.
+     * 
+     * @return {@code true} if the repository system is in offline mode, {@code false} otherwise.
+     */
+    boolean isOffline();
+
+    /**
+     * Indicates whether repositories declared in artifact descriptors should be ignored during transitive dependency
+     * collection. If enabled, only the repositories originally provided with the collect request will be considered.
+     * 
+     * @return {@code true} if additional repositories from artifact descriptors are ignored, {@code false} to merge
+     *         those with the originally specified repositories.
+     */
+    boolean isIgnoreArtifactDescriptorRepositories();
+
+    /**
+     * Gets the policy which controls whether resolutions errors from remote repositories should be cached.
+     * 
+     * @return The resolution error policy for this session or {@code null} if resolution errors should generally not be
+     *         cached.
+     */
+    ResolutionErrorPolicy getResolutionErrorPolicy();
+
+    /**
+     * Gets the policy which controls how errors related to reading artifact descriptors should be handled.
+     * 
+     * @return The descriptor error policy for this session or {@code null} if descriptor errors should generally not be
+     *         tolerated.
+     */
+    ArtifactDescriptorPolicy getArtifactDescriptorPolicy();
+
+    /**
+     * Gets the global checksum policy. If set, the global checksum policy overrides the checksum policies of the remote
+     * repositories being used for resolution.
+     * 
+     * @return The global checksum policy or {@code null}/empty if not set and the per-repository policies apply.
+     * @see RepositoryPolicy#CHECKSUM_POLICY_FAIL
+     * @see RepositoryPolicy#CHECKSUM_POLICY_IGNORE
+     * @see RepositoryPolicy#CHECKSUM_POLICY_WARN
+     */
+    String getChecksumPolicy();
+
+    /**
+     * Gets the global update policy. If set, the global update policy overrides the update policies of the remote
+     * repositories being used for resolution.
+     * 
+     * @return The global update policy or {@code null}/empty if not set and the per-repository policies apply.
+     * @see RepositoryPolicy#UPDATE_POLICY_ALWAYS
+     * @see RepositoryPolicy#UPDATE_POLICY_DAILY
+     * @see RepositoryPolicy#UPDATE_POLICY_NEVER
+     */
+    String getUpdatePolicy();
+
+    /**
+     * Gets the local repository used during this session. This is a convenience method for
+     * {@link LocalRepositoryManager#getRepository()}.
+     * 
+     * @return The local repository being during this session, never {@code null}.
+     */
+    LocalRepository getLocalRepository();
+
+    /**
+     * Gets the local repository manager used during this session.
+     * 
+     * @return The local repository manager used during this session, never {@code null}.
+     */
+    LocalRepositoryManager getLocalRepositoryManager();
+
+    /**
+     * Gets the workspace reader used during this session. If set, the workspace reader will usually be consulted first
+     * to resolve artifacts.
+     * 
+     * @return The workspace reader for this session or {@code null} if none.
+     */
+    WorkspaceReader getWorkspaceReader();
+
+    /**
+     * Gets the listener being notified of actions in the repository system.
+     * 
+     * @return The repository listener or {@code null} if none.
+     */
+    RepositoryListener getRepositoryListener();
+
+    /**
+     * Gets the listener being notified of uploads/downloads by the repository system.
+     * 
+     * @return The transfer listener or {@code null} if none.
+     */
+    TransferListener getTransferListener();
+
+    /**
+     * Gets the system properties to use, e.g. for processing of artifact descriptors. System properties are usually
+     * collected from the runtime environment like {@link System#getProperties()} and environment variables.
+     * 
+     * @return The (read-only) system properties, never {@code null}.
+     */
+    Map<String, String> getSystemProperties();
+
+    /**
+     * Gets the user properties to use, e.g. for processing of artifact descriptors. User properties are similar to
+     * system properties but are set on the discretion of the user and hence are considered of higher priority than
+     * system properties.
+     * 
+     * @return The (read-only) user properties, never {@code null}.
+     */
+    Map<String, String> getUserProperties();
+
+    /**
+     * Gets the configuration properties used to tweak internal aspects of the repository system (e.g. thread pooling,
+     * connector-specific behavior, etc.)
+     * 
+     * @return The (read-only) configuration properties, never {@code null}.
+     * @see ConfigurationProperties
+     */
+    Map<String, Object> getConfigProperties();
+
+    /**
+     * Gets the mirror selector to use for repositories discovered in artifact descriptors. Note that this selector is
+     * not used for remote repositories which are passed as request parameters to the repository system, those
+     * repositories are supposed to denote the effective repositories.
+     * 
+     * @return The mirror selector to use, never {@code null}.
+     * @see RepositorySystem#newResolutionRepositories(RepositorySystemSession, java.util.List)
+     */
+    MirrorSelector getMirrorSelector();
+
+    /**
+     * Gets the proxy selector to use for repositories discovered in artifact descriptors. Note that this selector is
+     * not used for remote repositories which are passed as request parameters to the repository system, those
+     * repositories are supposed to have their proxy (if any) already set.
+     * 
+     * @return The proxy selector to use, never {@code null}.
+     * @see org.eclipse.aether.repository.RemoteRepository#getProxy()
+     * @see RepositorySystem#newResolutionRepositories(RepositorySystemSession, java.util.List)
+     */
+    ProxySelector getProxySelector();
+
+    /**
+     * Gets the authentication selector to use for repositories discovered in artifact descriptors. Note that this
+     * selector is not used for remote repositories which are passed as request parameters to the repository system,
+     * those repositories are supposed to have their authentication (if any) already set.
+     * 
+     * @return The authentication selector to use, never {@code null}.
+     * @see org.eclipse.aether.repository.RemoteRepository#getAuthentication()
+     * @see RepositorySystem#newResolutionRepositories(RepositorySystemSession, java.util.List)
+     */
+    AuthenticationSelector getAuthenticationSelector();
+
+    /**
+     * Gets the registry of artifact types recognized by this session, for instance when processing artifact
+     * descriptors.
+     * 
+     * @return The artifact type registry, never {@code null}.
+     */
+    ArtifactTypeRegistry getArtifactTypeRegistry();
+
+    /**
+     * Gets the dependency traverser to use for building dependency graphs.
+     * 
+     * @return The dependency traverser to use for building dependency graphs or {@code null} if dependencies are
+     *         unconditionally traversed.
+     */
+    DependencyTraverser getDependencyTraverser();
+
+    /**
+     * Gets the dependency manager to use for building dependency graphs.
+     * 
+     * @return The dependency manager to use for building dependency graphs or {@code null} if dependency management is
+     *         not performed.
+     */
+    DependencyManager getDependencyManager();
+
+    /**
+     * Gets the dependency selector to use for building dependency graphs.
+     * 
+     * @return The dependency selector to use for building dependency graphs or {@code null} if dependencies are
+     *         unconditionally included.
+     */
+    DependencySelector getDependencySelector();
+
+    /**
+     * Gets the version filter to use for building dependency graphs.
+     * 
+     * @return The version filter to use for building dependency graphs or {@code null} if versions aren't filtered.
+     */
+    VersionFilter getVersionFilter();
+
+    /**
+     * Gets the dependency graph transformer to use for building dependency graphs.
+     * 
+     * @return The dependency graph transformer to use for building dependency graphs or {@code null} if none.
+     */
+    DependencyGraphTransformer getDependencyGraphTransformer();
+
+    /**
+     * Gets the custom data associated with this session.
+     * 
+     * @return The session data, never {@code null}.
+     */
+    SessionData getData();
+
+    /**
+     * Gets the cache the repository system may use to save data for future reuse during the session.
+     * 
+     * @return The repository cache or {@code null} if none.
+     */
+    RepositoryCache getCache();
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/RequestTrace.java b/org.argeo.slc.repo/src/org/eclipse/aether/RequestTrace.java
new file mode 100644 (file)
index 0000000..c6afa8e
--- /dev/null
@@ -0,0 +1,108 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether;
+
+/**
+ * A trace of nested requests that are performed by the repository system. This trace information can be used to
+ * correlate repository events with higher level operations in the application code that eventually caused the events. A
+ * single trace can carry an arbitrary object as data which is meant to describe a request/operation that is currently
+ * executed. For call hierarchies within the repository system itself, this data will usually be the {@code *Request}
+ * object that is currently processed. When invoking methods on the repository system, client code may provide a request
+ * trace that has been prepopulated with whatever data is useful for the application to indicate its state for later
+ * evaluation when processing the repository events.
+ * 
+ * @see RepositoryEvent#getTrace()
+ */
+public class RequestTrace
+{
+
+    private final RequestTrace parent;
+
+    private final Object data;
+
+    /**
+     * Creates a child of the specified request trace. This method is basically a convenience that will invoke
+     * {@link RequestTrace#newChild(Object) parent.newChild()} when the specified parent trace is not {@code null} or
+     * otherwise instantiante a new root trace.
+     * 
+     * @param parent The parent request trace, may be {@code null}.
+     * @param data The data to associate with the child trace, may be {@code null}.
+     * @return The child trace, never {@code null}.
+     */
+    public static RequestTrace newChild( RequestTrace parent, Object data )
+    {
+        if ( parent == null )
+        {
+            return new RequestTrace( data );
+        }
+        return parent.newChild( data );
+    }
+
+    /**
+     * Creates a new root trace with the specified data.
+     * 
+     * @param data The data to associate with the trace, may be {@code null}.
+     */
+    public RequestTrace( Object data )
+    {
+        this( null, data );
+    }
+
+    /**
+     * Creates a new trace with the specified data and parent
+     * 
+     * @param parent The parent trace, may be {@code null} for a root trace.
+     * @param data The data to associate with the trace, may be {@code null}.
+     */
+    protected RequestTrace( RequestTrace parent, Object data )
+    {
+        this.parent = parent;
+        this.data = data;
+    }
+
+    /**
+     * Gets the data associated with this trace.
+     * 
+     * @return The data associated with this trace or {@code null} if none.
+     */
+    public final Object getData()
+    {
+        return data;
+    }
+
+    /**
+     * Gets the parent of this trace.
+     * 
+     * @return The parent of this trace or {@code null} if this is the root of the trace stack.
+     */
+    public final RequestTrace getParent()
+    {
+        return parent;
+    }
+
+    /**
+     * Creates a new child of this trace.
+     * 
+     * @param data The data to associate with the child, may be {@code null}.
+     * @return The child trace, never {@code null}.
+     */
+    public RequestTrace newChild( Object data )
+    {
+        return new RequestTrace( this, data );
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( getData() );
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/SessionData.java b/org.argeo.slc.repo/src/org/eclipse/aether/SessionData.java
new file mode 100644 (file)
index 0000000..92930e7
--- /dev/null
@@ -0,0 +1,57 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether;
+
+/**
+ * A container for data that is specific to a repository system session. Both components within the repository system
+ * and clients of the system may use this storage to associate arbitrary data with a session.
+ * <p>
+ * Unlike a cache, this session data is not subject to purging. For this same reason, session data should also not be
+ * abused as a cache (i.e. for storing values that can be re-calculated) to avoid memory exhaustion.
+ * <p>
+ * <strong>Note:</strong> Actual implementations must be thread-safe.
+ * 
+ * @see RepositorySystemSession#getData()
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface SessionData
+{
+
+    /**
+     * Associates the specified session data with the given key.
+     * 
+     * @param key The key under which to store the session data, must not be {@code null}.
+     * @param value The data to associate with the key, may be {@code null} to remove the mapping.
+     */
+    void set( Object key, Object value );
+
+    /**
+     * Associates the specified session data with the given key if the key is currently mapped to the given value. This
+     * method provides an atomic compare-and-update of some key's value.
+     * 
+     * @param key The key under which to store the session data, must not be {@code null}.
+     * @param oldValue The expected data currently associated with the key, may be {@code null}.
+     * @param newValue The data to associate with the key, may be {@code null} to remove the mapping.
+     * @return {@code true} if the key mapping was successfully updated from the old value to the new value,
+     *         {@code false} if the current key mapping didn't match the expected value and was not updated.
+     */
+    boolean set( Object key, Object oldValue, Object newValue );
+
+    /**
+     * Gets the session data associated with the specified key.
+     * 
+     * @param key The key for which to retrieve the session data, must not be {@code null}.
+     * @return The session data associated with the key or {@code null} if none.
+     */
+    Object get( Object key );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/SyncContext.java b/org.argeo.slc.repo/src/org/eclipse/aether/SyncContext.java
new file mode 100644 (file)
index 0000000..a05d512
--- /dev/null
@@ -0,0 +1,67 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether;
+
+import java.io.Closeable;
+import java.util.Collection;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * A synchronization context used to coordinate concurrent access to artifacts or metadatas. The typical usage of a
+ * synchronization context looks like this:
+ * 
+ * <pre>
+ * SyncContext syncContext = repositorySystem.newSyncContext( ... );
+ * try {
+ *     syncContext.acquire( artifacts, metadatas );
+ *     // work with the artifacts and metadatas
+ * } finally {
+ *     syncContext.close();
+ * }
+ * </pre>
+ * 
+ * Within one thread, synchronization contexts may be nested which can naturally happen in a hierarchy of method calls.
+ * The nested synchronization contexts may also acquire overlapping sets of artifacts/metadatas as long as the following
+ * conditions are met. If the outer-most context holding a particular resource is exclusive, that resource can be
+ * reacquired in any nested context. If however the outer-most context is shared, the resource may only be reacquired by
+ * nested contexts if these are also shared.
+ * <p>
+ * A synchronization context is meant to be utilized by only one thread and as such is not thread-safe.
+ * <p>
+ * Note that the level of actual synchronization is subject to the implementation and might range from OS-wide to none.
+ * 
+ * @see RepositorySystem#newSyncContext(RepositorySystemSession, boolean)
+ */
+public interface SyncContext
+    extends Closeable
+{
+
+    /**
+     * Acquires synchronized access to the specified artifacts and metadatas. The invocation will potentially block
+     * until all requested resources can be acquired by the calling thread. Acquiring resources that are already
+     * acquired by this synchronization context has no effect. Please also see the class-level documentation for
+     * information regarding reentrancy. The method may be invoked multiple times on a synchronization context until all
+     * desired resources have been acquired.
+     * 
+     * @param artifacts The artifacts to acquire, may be {@code null} or empty if none.
+     * @param metadatas The metadatas to acquire, may be {@code null} or empty if none.
+     */
+    void acquire( Collection<? extends Artifact> artifacts, Collection<? extends Metadata> metadatas );
+
+    /**
+     * Releases all previously acquired artifacts/metadatas. If no resources have been acquired before or if this
+     * synchronization context has already been closed, this method does nothing.
+     */
+    void close();
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/artifact/AbstractArtifact.java b/org.argeo.slc.repo/src/org/eclipse/aether/artifact/AbstractArtifact.java
new file mode 100644 (file)
index 0000000..2944ff8
--- /dev/null
@@ -0,0 +1,221 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.artifact;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A skeleton class for artifacts.
+ */
+public abstract class AbstractArtifact
+    implements Artifact
+{
+
+    private static final String SNAPSHOT = "SNAPSHOT";
+
+    private static final Pattern SNAPSHOT_TIMESTAMP = Pattern.compile( "^(.*-)?([0-9]{8}\\.[0-9]{6}-[0-9]+)$" );
+
+    public boolean isSnapshot()
+    {
+        return isSnapshot( getVersion() );
+    }
+
+    private static boolean isSnapshot( String version )
+    {
+        return version.endsWith( SNAPSHOT ) || SNAPSHOT_TIMESTAMP.matcher( version ).matches();
+    }
+
+    public String getBaseVersion()
+    {
+        return toBaseVersion( getVersion() );
+    }
+
+    private static String toBaseVersion( String version )
+    {
+        String baseVersion;
+
+        if ( version == null )
+        {
+            baseVersion = version;
+        }
+        else if ( version.startsWith( "[" ) || version.startsWith( "(" ) )
+        {
+            baseVersion = version;
+        }
+        else
+        {
+            Matcher m = SNAPSHOT_TIMESTAMP.matcher( version );
+            if ( m.matches() )
+            {
+                if ( m.group( 1 ) != null )
+                {
+                    baseVersion = m.group( 1 ) + SNAPSHOT;
+                }
+                else
+                {
+                    baseVersion = SNAPSHOT;
+                }
+            }
+            else
+            {
+                baseVersion = version;
+            }
+        }
+
+        return baseVersion;
+    }
+
+    /**
+     * Creates a new artifact with the specified coordinates, properties and file.
+     * 
+     * @param version The version of the artifact, may be {@code null}.
+     * @param properties The properties of the artifact, may be {@code null} if none. The method may assume immutability
+     *            of the supplied map, i.e. need not copy it.
+     * @param file The resolved file of the artifact, may be {@code null}.
+     * @return The new artifact instance, never {@code null}.
+     */
+    private Artifact newInstance( String version, Map<String, String> properties, File file )
+    {
+        return new DefaultArtifact( getGroupId(), getArtifactId(), getClassifier(), getExtension(), version, file,
+                                    properties );
+    }
+
+    public Artifact setVersion( String version )
+    {
+        String current = getVersion();
+        if ( current.equals( version ) || ( version == null && current.length() <= 0 ) )
+        {
+            return this;
+        }
+        return newInstance( version, getProperties(), getFile() );
+    }
+
+    public Artifact setFile( File file )
+    {
+        File current = getFile();
+        if ( ( current == null ) ? file == null : current.equals( file ) )
+        {
+            return this;
+        }
+        return newInstance( getVersion(), getProperties(), file );
+    }
+
+    public Artifact setProperties( Map<String, String> properties )
+    {
+        Map<String, String> current = getProperties();
+        if ( current.equals( properties ) || ( properties == null && current.isEmpty() ) )
+        {
+            return this;
+        }
+        return newInstance( getVersion(), copyProperties( properties ), getFile() );
+    }
+
+    public String getProperty( String key, String defaultValue )
+    {
+        String value = getProperties().get( key );
+        return ( value != null ) ? value : defaultValue;
+    }
+
+    /**
+     * Copies the specified artifact properties. This utility method should be used when creating new artifact instances
+     * with caller-supplied properties.
+     * 
+     * @param properties The properties to copy, may be {@code null}.
+     * @return The copied and read-only properties, never {@code null}.
+     */
+    protected static Map<String, String> copyProperties( Map<String, String> properties )
+    {
+        if ( properties != null && !properties.isEmpty() )
+        {
+            return Collections.unmodifiableMap( new HashMap<String, String>( properties ) );
+        }
+        else
+        {
+            return Collections.emptyMap();
+        }
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder buffer = new StringBuilder( 128 );
+        buffer.append( getGroupId() );
+        buffer.append( ':' ).append( getArtifactId() );
+        buffer.append( ':' ).append( getExtension() );
+        if ( getClassifier().length() > 0 )
+        {
+            buffer.append( ':' ).append( getClassifier() );
+        }
+        buffer.append( ':' ).append( getVersion() );
+        return buffer.toString();
+    }
+
+    /**
+     * Compares this artifact with the specified object.
+     * 
+     * @param obj The object to compare this artifact against, may be {@code null}.
+     * @return {@code true} if and only if the specified object is another {@link Artifact} with equal coordinates,
+     *         properties and file, {@code false} otherwise.
+     */
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( obj == this )
+        {
+            return true;
+        }
+        else if ( !( obj instanceof Artifact ) )
+        {
+            return false;
+        }
+
+        Artifact that = (Artifact) obj;
+
+        return getArtifactId().equals( that.getArtifactId() ) && getGroupId().equals( that.getGroupId() )
+            && getVersion().equals( that.getVersion() ) && getExtension().equals( that.getExtension() )
+            && getClassifier().equals( that.getClassifier() ) && eq( getFile(), that.getFile() )
+            && getProperties().equals( that.getProperties() );
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    /**
+     * Returns a hash code for this artifact.
+     * 
+     * @return A hash code for the artifact.
+     */
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + getGroupId().hashCode();
+        hash = hash * 31 + getArtifactId().hashCode();
+        hash = hash * 31 + getExtension().hashCode();
+        hash = hash * 31 + getClassifier().hashCode();
+        hash = hash * 31 + getVersion().hashCode();
+        hash = hash * 31 + hash( getFile() );
+        return hash;
+    }
+
+    private static int hash( Object obj )
+    {
+        return ( obj != null ) ? obj.hashCode() : 0;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/artifact/Artifact.java b/org.argeo.slc.repo/src/org/eclipse/aether/artifact/Artifact.java
new file mode 100644 (file)
index 0000000..5eef695
--- /dev/null
@@ -0,0 +1,134 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.artifact;
+
+import java.io.File;
+import java.util.Map;
+
+/**
+ * A specific artifact. In a nutshell, an artifact has identifying coordinates and optionally a file that denotes its
+ * data. <em>Note:</em> Artifact instances are supposed to be immutable, e.g. any exposed mutator method returns a new
+ * artifact instance and leaves the original instance unchanged. <em>Note:</em> Implementors are strongly advised to
+ * inherit from {@link AbstractArtifact} instead of directly implementing this interface.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface Artifact
+{
+
+    /**
+     * Gets the group identifier of this artifact, for example "org.apache.maven".
+     * 
+     * @return The group identifier, never {@code null}.
+     */
+    String getGroupId();
+
+    /**
+     * Gets the artifact identifier of this artifact, for example "maven-model".
+     * 
+     * @return The artifact identifier, never {@code null}.
+     */
+    String getArtifactId();
+
+    /**
+     * Gets the version of this artifact, for example "1.0-20100529-1213". Note that in case of meta versions like
+     * "1.0-SNAPSHOT", the artifact's version depends on the state of the artifact. Artifacts that have been resolved or
+     * deployed will usually have the meta version expanded.
+     * 
+     * @return The version, never {@code null}.
+     */
+    String getVersion();
+
+    /**
+     * Sets the version of the artifact.
+     * 
+     * @param version The version of this artifact, may be {@code null} or empty.
+     * @return The new artifact, never {@code null}.
+     */
+    Artifact setVersion( String version );
+
+    /**
+     * Gets the base version of this artifact, for example "1.0-SNAPSHOT". In contrast to the {@link #getVersion()}, the
+     * base version will always refer to the unresolved meta version.
+     * 
+     * @return The base version, never {@code null}.
+     */
+    String getBaseVersion();
+
+    /**
+     * Determines whether this artifact uses a snapshot version.
+     * 
+     * @return {@code true} if the artifact is a snapshot, {@code false} otherwise.
+     */
+    boolean isSnapshot();
+
+    /**
+     * Gets the classifier of this artifact, for example "sources".
+     * 
+     * @return The classifier or an empty string if none, never {@code null}.
+     */
+    String getClassifier();
+
+    /**
+     * Gets the (file) extension of this artifact, for example "jar" or "tar.gz".
+     * 
+     * @return The file extension (without leading period), never {@code null}.
+     */
+    String getExtension();
+
+    /**
+     * Gets the file of this artifact. Note that only resolved artifacts have a file associated with them. In general,
+     * callers must not assume any relationship between an artifact's filename and its coordinates.
+     * 
+     * @return The file or {@code null} if the artifact isn't resolved.
+     */
+    File getFile();
+
+    /**
+     * Sets the file of the artifact.
+     * 
+     * @param file The file of the artifact, may be {@code null}
+     * @return The new artifact, never {@code null}.
+     */
+    Artifact setFile( File file );
+
+    /**
+     * Gets the specified property.
+     * 
+     * @param key The name of the property, must not be {@code null}.
+     * @param defaultValue The default value to return in case the property is not set, may be {@code null}.
+     * @return The requested property value or {@code null} if the property is not set and no default value was
+     *         provided.
+     * @see ArtifactProperties
+     */
+    String getProperty( String key, String defaultValue );
+
+    /**
+     * Gets the properties of this artifact. Clients may use these properties to associate non-persistent values with an
+     * artifact that help later processing when the artifact gets passed around within the application.
+     * 
+     * @return The (read-only) properties, never {@code null}.
+     * @see ArtifactProperties
+     */
+    Map<String, String> getProperties();
+
+    /**
+     * Sets the properties for the artifact. Note that these properties exist merely in memory and are not persisted
+     * when the artifact gets installed/deployed to a repository.
+     * 
+     * @param properties The properties for the artifact, may be {@code null}.
+     * @return The new artifact, never {@code null}.
+     * @see ArtifactProperties
+     */
+    Artifact setProperties( Map<String, String> properties );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/artifact/ArtifactProperties.java b/org.argeo.slc.repo/src/org/eclipse/aether/artifact/ArtifactProperties.java
new file mode 100644 (file)
index 0000000..7fbea04
--- /dev/null
@@ -0,0 +1,65 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.artifact;
+
+/**
+ * The keys for common properties of artifacts.
+ * 
+ * @see Artifact#getProperties()
+ */
+public final class ArtifactProperties
+{
+
+    /**
+     * A high-level characterization of the artifact, e.g. "maven-plugin" or "test-jar".
+     * 
+     * @see ArtifactType#getId()
+     */
+    public static final String TYPE = "type";
+
+    /**
+     * The programming language this artifact is relevant for, e.g. "java" or "none".
+     */
+    public static final String LANGUAGE = "language";
+
+    /**
+     * The (expected) path to the artifact on the local filesystem. An artifact which has this property set is assumed
+     * to be not present in any regular repository and likewise has no artifact descriptor. Artifact resolution will
+     * verify the path and resolve the artifact if the path actually denotes an existing file. If the path isn't valid,
+     * resolution will fail and no attempts to search local/remote repositories are made.
+     */
+    public static final String LOCAL_PATH = "localPath";
+
+    /**
+     * A boolean flag indicating whether the artifact presents some kind of bundle that physically includes its
+     * dependencies, e.g. a fat WAR.
+     */
+    public static final String INCLUDES_DEPENDENCIES = "includesDependencies";
+
+    /**
+     * A boolean flag indicating whether the artifact is meant to be used for the compile/runtime/test build path of a
+     * consumer project.
+     */
+    public static final String CONSTITUTES_BUILD_PATH = "constitutesBuildPath";
+
+    /**
+     * The URL to a web page from which the artifact can be manually downloaded. This URL is not contacted by the
+     * repository system but serves as a pointer for the end user to assist in getting artifacts that are not published
+     * in a proper repository.
+     */
+    public static final String DOWNLOAD_URL = "downloadUrl";
+
+    private ArtifactProperties()
+    {
+        // hide constructor
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/artifact/ArtifactType.java b/org.argeo.slc.repo/src/org/eclipse/aether/artifact/ArtifactType.java
new file mode 100644 (file)
index 0000000..174c3c5
--- /dev/null
@@ -0,0 +1,58 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.artifact;
+
+import java.util.Map;
+
+/**
+ * An artifact type describing artifact characteristics/properties that are common for certain artifacts. Artifact types
+ * are a means to simplify the description of an artifact by referring to an artifact type instead of specifying the
+ * various properties individually.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ * @see ArtifactTypeRegistry
+ * @see DefaultArtifact#DefaultArtifact(String, String, String, String, String, ArtifactType)
+ */
+public interface ArtifactType
+{
+
+    /**
+     * Gets the identifier of this type, e.g. "maven-plugin" or "test-jar".
+     * 
+     * @return The identifier of this type, never {@code null}.
+     * @see ArtifactProperties#TYPE
+     */
+    String getId();
+
+    /**
+     * Gets the file extension to use for artifacts of this type (unless explicitly overridden by the artifact).
+     * 
+     * @return The usual file extension, never {@code null}.
+     */
+    String getExtension();
+
+    /**
+     * Gets the classifier to use for artifacts of this type (unless explicitly overridden by the artifact).
+     * 
+     * @return The usual classifier or an empty string if none, never {@code null}.
+     */
+    String getClassifier();
+
+    /**
+     * Gets the properties to use for artifacts of this type (unless explicitly overridden by the artifact).
+     * 
+     * @return The (read-only) properties, never {@code null}.
+     * @see ArtifactProperties
+     */
+    Map<String, String> getProperties();
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/artifact/ArtifactTypeRegistry.java b/org.argeo.slc.repo/src/org/eclipse/aether/artifact/ArtifactTypeRegistry.java
new file mode 100644 (file)
index 0000000..2addff1
--- /dev/null
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.artifact;
+
+/**
+ * A registry of known artifact types.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getArtifactTypeRegistry()
+ */
+public interface ArtifactTypeRegistry
+{
+
+    /**
+     * Gets the artifact type with the specified identifier.
+     * 
+     * @param typeId The identifier of the type, must not be {@code null}.
+     * @return The artifact type or {@code null} if no type with the requested identifier exists.
+     */
+    ArtifactType get( String typeId );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/artifact/DefaultArtifact.java b/org.argeo.slc.repo/src/org/eclipse/aether/artifact/DefaultArtifact.java
new file mode 100644 (file)
index 0000000..9971034
--- /dev/null
@@ -0,0 +1,276 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.artifact;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A simple artifact. <em>Note:</em> Instances of this class are immutable and the exposed mutators return new objects
+ * rather than changing the current instance.
+ */
+public final class DefaultArtifact
+    extends AbstractArtifact
+{
+
+    private final String groupId;
+
+    private final String artifactId;
+
+    private final String version;
+
+    private final String classifier;
+
+    private final String extension;
+
+    private final File file;
+
+    private final Map<String, String> properties;
+
+    /**
+     * Creates a new artifact with the specified coordinates. If not specified in the artifact coordinates, the
+     * artifact's extension defaults to {@code jar} and classifier to an empty string.
+     * 
+     * @param coords The artifact coordinates in the format
+     *            {@code <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>}, must not be {@code null}.
+     */
+    public DefaultArtifact( String coords )
+    {
+        this( coords, Collections.<String, String> emptyMap() );
+    }
+
+    /**
+     * Creates a new artifact with the specified coordinates and properties. If not specified in the artifact
+     * coordinates, the artifact's extension defaults to {@code jar} and classifier to an empty string.
+     * 
+     * @param coords The artifact coordinates in the format
+     *            {@code <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>}, must not be {@code null}.
+     * @param properties The artifact properties, may be {@code null}.
+     */
+    public DefaultArtifact( String coords, Map<String, String> properties )
+    {
+        Pattern p = Pattern.compile( "([^: ]+):([^: ]+)(:([^: ]*)(:([^: ]+))?)?:([^: ]+)" );
+        Matcher m = p.matcher( coords );
+        if ( !m.matches() )
+        {
+            throw new IllegalArgumentException( "Bad artifact coordinates " + coords
+                + ", expected format is <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>" );
+        }
+        groupId = m.group( 1 );
+        artifactId = m.group( 2 );
+        extension = get( m.group( 4 ), "jar" );
+        classifier = get( m.group( 6 ), "" );
+        version = m.group( 7 );
+        file = null;
+        this.properties = copyProperties( properties );
+    }
+
+    private static String get( String value, String defaultValue )
+    {
+        return ( value == null || value.length() <= 0 ) ? defaultValue : value;
+    }
+
+    /**
+     * Creates a new artifact with the specified coordinates and no classifier. Passing {@code null} for any of the
+     * coordinates is equivalent to specifying an empty string.
+     * 
+     * @param groupId The group identifier of the artifact, may be {@code null}.
+     * @param artifactId The artifact identifier of the artifact, may be {@code null}.
+     * @param extension The file extension of the artifact, may be {@code null}.
+     * @param version The version of the artifact, may be {@code null}.
+     */
+    public DefaultArtifact( String groupId, String artifactId, String extension, String version )
+    {
+        this( groupId, artifactId, "", extension, version );
+    }
+
+    /**
+     * Creates a new artifact with the specified coordinates. Passing {@code null} for any of the coordinates is
+     * equivalent to specifying an empty string.
+     * 
+     * @param groupId The group identifier of the artifact, may be {@code null}.
+     * @param artifactId The artifact identifier of the artifact, may be {@code null}.
+     * @param classifier The classifier of the artifact, may be {@code null}.
+     * @param extension The file extension of the artifact, may be {@code null}.
+     * @param version The version of the artifact, may be {@code null}.
+     */
+    public DefaultArtifact( String groupId, String artifactId, String classifier, String extension, String version )
+    {
+        this( groupId, artifactId, classifier, extension, version, null, (File) null );
+    }
+
+    /**
+     * Creates a new artifact with the specified coordinates. Passing {@code null} for any of the coordinates is
+     * equivalent to specifying an empty string. The optional artifact type provided to this constructor will be used to
+     * determine the artifact's classifier and file extension if the corresponding arguments for this constructor are
+     * {@code null}.
+     * 
+     * @param groupId The group identifier of the artifact, may be {@code null}.
+     * @param artifactId The artifact identifier of the artifact, may be {@code null}.
+     * @param classifier The classifier of the artifact, may be {@code null}.
+     * @param extension The file extension of the artifact, may be {@code null}.
+     * @param version The version of the artifact, may be {@code null}.
+     * @param type The artifact type from which to query classifier, file extension and properties, may be {@code null}.
+     */
+    public DefaultArtifact( String groupId, String artifactId, String classifier, String extension, String version,
+                            ArtifactType type )
+    {
+        this( groupId, artifactId, classifier, extension, version, null, type );
+    }
+
+    /**
+     * Creates a new artifact with the specified coordinates and properties. Passing {@code null} for any of the
+     * coordinates is equivalent to specifying an empty string. The optional artifact type provided to this constructor
+     * will be used to determine the artifact's classifier and file extension if the corresponding arguments for this
+     * constructor are {@code null}. If the artifact type specifies properties, those will get merged with the
+     * properties passed directly into the constructor, with the latter properties taking precedence.
+     * 
+     * @param groupId The group identifier of the artifact, may be {@code null}.
+     * @param artifactId The artifact identifier of the artifact, may be {@code null}.
+     * @param classifier The classifier of the artifact, may be {@code null}.
+     * @param extension The file extension of the artifact, may be {@code null}.
+     * @param version The version of the artifact, may be {@code null}.
+     * @param properties The properties of the artifact, may be {@code null} if none.
+     * @param type The artifact type from which to query classifier, file extension and properties, may be {@code null}.
+     */
+    public DefaultArtifact( String groupId, String artifactId, String classifier, String extension, String version,
+                            Map<String, String> properties, ArtifactType type )
+    {
+        this.groupId = emptify( groupId );
+        this.artifactId = emptify( artifactId );
+        if ( classifier != null || type == null )
+        {
+            this.classifier = emptify( classifier );
+        }
+        else
+        {
+            this.classifier = emptify( type.getClassifier() );
+        }
+        if ( extension != null || type == null )
+        {
+            this.extension = emptify( extension );
+        }
+        else
+        {
+            this.extension = emptify( type.getExtension() );
+        }
+        this.version = emptify( version );
+        this.file = null;
+        this.properties = merge( properties, ( type != null ) ? type.getProperties() : null );
+    }
+
+    private static Map<String, String> merge( Map<String, String> dominant, Map<String, String> recessive )
+    {
+        Map<String, String> properties;
+
+        if ( ( dominant == null || dominant.isEmpty() ) && ( recessive == null || recessive.isEmpty() ) )
+        {
+            properties = Collections.emptyMap();
+        }
+        else
+        {
+            properties = new HashMap<String, String>();
+            if ( recessive != null )
+            {
+                properties.putAll( recessive );
+            }
+            if ( dominant != null )
+            {
+                properties.putAll( dominant );
+            }
+            properties = Collections.unmodifiableMap( properties );
+        }
+
+        return properties;
+    }
+
+    /**
+     * Creates a new artifact with the specified coordinates, properties and file. Passing {@code null} for any of the
+     * coordinates is equivalent to specifying an empty string.
+     * 
+     * @param groupId The group identifier of the artifact, may be {@code null}.
+     * @param artifactId The artifact identifier of the artifact, may be {@code null}.
+     * @param classifier The classifier of the artifact, may be {@code null}.
+     * @param extension The file extension of the artifact, may be {@code null}.
+     * @param version The version of the artifact, may be {@code null}.
+     * @param properties The properties of the artifact, may be {@code null} if none.
+     * @param file The resolved file of the artifact, may be {@code null}.
+     */
+    public DefaultArtifact( String groupId, String artifactId, String classifier, String extension, String version,
+                            Map<String, String> properties, File file )
+    {
+        this.groupId = emptify( groupId );
+        this.artifactId = emptify( artifactId );
+        this.classifier = emptify( classifier );
+        this.extension = emptify( extension );
+        this.version = emptify( version );
+        this.file = file;
+        this.properties = copyProperties( properties );
+    }
+
+    DefaultArtifact( String groupId, String artifactId, String classifier, String extension, String version, File file,
+                     Map<String, String> properties )
+    {
+        // NOTE: This constructor assumes immutability of the provided properties, for internal use only
+        this.groupId = emptify( groupId );
+        this.artifactId = emptify( artifactId );
+        this.classifier = emptify( classifier );
+        this.extension = emptify( extension );
+        this.version = emptify( version );
+        this.file = file;
+        this.properties = properties;
+    }
+
+    private static String emptify( String str )
+    {
+        return ( str == null ) ? "" : str;
+    }
+
+    public String getGroupId()
+    {
+        return groupId;
+    }
+
+    public String getArtifactId()
+    {
+        return artifactId;
+    }
+
+    public String getVersion()
+    {
+        return version;
+    }
+
+    public String getClassifier()
+    {
+        return classifier;
+    }
+
+    public String getExtension()
+    {
+        return extension;
+    }
+
+    public File getFile()
+    {
+        return file;
+    }
+
+    public Map<String, String> getProperties()
+    {
+        return properties;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/artifact/DefaultArtifactType.java b/org.argeo.slc.repo/src/org/eclipse/aether/artifact/DefaultArtifactType.java
new file mode 100644 (file)
index 0000000..b30cd12
--- /dev/null
@@ -0,0 +1,137 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.artifact;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A simple artifact type.
+ */
+public final class DefaultArtifactType
+    implements ArtifactType
+{
+
+    private final String id;
+
+    private final String extension;
+
+    private final String classifier;
+
+    private final Map<String, String> properties;
+
+    /**
+     * Creates a new artifact type with the specified identifier. This constructor assumes the usual file extension
+     * equals the given type id and that the usual classifier is empty. Additionally, the properties
+     * {@link ArtifactProperties#LANGUAGE}, {@link ArtifactProperties#CONSTITUTES_BUILD_PATH} and
+     * {@link ArtifactProperties#INCLUDES_DEPENDENCIES} will be set to {@code "none"}, {@code true} and {@code false},
+     * respectively.
+     * 
+     * @param id The identifier of the type which will also be used as the value for the {@link ArtifactProperties#TYPE}
+     *            property, must not be {@code null} or empty.
+     */
+    public DefaultArtifactType( String id )
+    {
+        this( id, id, "", "none", false, false );
+    }
+
+    /**
+     * Creates a new artifact type with the specified properties. Additionally, the properties
+     * {@link ArtifactProperties#CONSTITUTES_BUILD_PATH} and {@link ArtifactProperties#INCLUDES_DEPENDENCIES} will be
+     * set to {@code true} and {@code false}, respectively.
+     * 
+     * @param id The identifier of the type which will also be used as the value for the {@link ArtifactProperties#TYPE}
+     *            property, must not be {@code null} or empty.
+     * @param extension The usual file extension for artifacts of this type, may be {@code null}.
+     * @param classifier The usual classifier for artifacts of this type, may be {@code null}.
+     * @param language The value for the {@link ArtifactProperties#LANGUAGE} property, may be {@code null}.
+     */
+    public DefaultArtifactType( String id, String extension, String classifier, String language )
+    {
+        this( id, extension, classifier, language, true, false );
+    }
+
+    /**
+     * Creates a new artifact type with the specified properties.
+     * 
+     * @param id The identifier of the type which will also be used as the value for the {@link ArtifactProperties#TYPE}
+     *            property, must not be {@code null} or empty.
+     * @param extension The usual file extension for artifacts of this type, may be {@code null}.
+     * @param classifier The usual classifier for artifacts of this type, may be {@code null}.
+     * @param language The value for the {@link ArtifactProperties#LANGUAGE} property, may be {@code null}.
+     * @param constitutesBuildPath The value for the {@link ArtifactProperties#CONSTITUTES_BUILD_PATH} property.
+     * @param includesDependencies The value for the {@link ArtifactProperties#INCLUDES_DEPENDENCIES} property.
+     */
+    public DefaultArtifactType( String id, String extension, String classifier, String language,
+                                boolean constitutesBuildPath, boolean includesDependencies )
+    {
+        if ( id == null || id.length() < 0 )
+        {
+            throw new IllegalArgumentException( "no type id specified" );
+        }
+        this.id = id;
+        this.extension = emptify( extension );
+        this.classifier = emptify( classifier );
+        Map<String, String> props = new HashMap<String, String>();
+        props.put( ArtifactProperties.TYPE, id );
+        props.put( ArtifactProperties.LANGUAGE, ( language != null && language.length() > 0 ) ? language : "none" );
+        props.put( ArtifactProperties.INCLUDES_DEPENDENCIES, Boolean.toString( includesDependencies ) );
+        props.put( ArtifactProperties.CONSTITUTES_BUILD_PATH, Boolean.toString( constitutesBuildPath ) );
+        properties = Collections.unmodifiableMap( props );
+    }
+
+    /**
+     * Creates a new artifact type with the specified properties.
+     * 
+     * @param id The identifier of the type, must not be {@code null} or empty.
+     * @param extension The usual file extension for artifacts of this type, may be {@code null}.
+     * @param classifier The usual classifier for artifacts of this type, may be {@code null}.
+     * @param properties The properties for artifacts of this type, may be {@code null}.
+     */
+    public DefaultArtifactType( String id, String extension, String classifier, Map<String, String> properties )
+    {
+        if ( id == null || id.length() < 0 )
+        {
+            throw new IllegalArgumentException( "no type id specified" );
+        }
+        this.id = id;
+        this.extension = emptify( extension );
+        this.classifier = emptify( classifier );
+        this.properties = AbstractArtifact.copyProperties( properties );
+    }
+
+    private static String emptify( String str )
+    {
+        return ( str == null ) ? "" : str;
+    }
+
+    public String getId()
+    {
+        return id;
+    }
+
+    public String getExtension()
+    {
+        return extension;
+    }
+
+    public String getClassifier()
+    {
+        return classifier;
+    }
+
+    public Map<String, String> getProperties()
+    {
+        return properties;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/artifact/package-info.java b/org.argeo.slc.repo/src/org/eclipse/aether/artifact/package-info.java
new file mode 100644 (file)
index 0000000..6d676d1
--- /dev/null
@@ -0,0 +1,15 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+/**
+ * The definition of an artifact, that is the primary entity managed by the repository system.
+ */
+package org.eclipse.aether.artifact;
+
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/collection/CollectRequest.java b/org.argeo.slc.repo/src/org/eclipse/aether/collection/CollectRequest.java
new file mode 100644 (file)
index 0000000..8568385
--- /dev/null
@@ -0,0 +1,347 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.collection;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A request to collect the transitive dependencies and to build a dependency graph from them. There are three ways to
+ * create a dependency graph. First, only the root dependency can be given. Second, a root dependency and direct
+ * dependencies can be specified in which case the specified direct dependencies are merged with the direct dependencies
+ * retrieved from the artifact descriptor of the root dependency. And last, only direct dependencies can be specified in
+ * which case the root node of the resulting graph has no associated dependency.
+ * 
+ * @see RepositorySystem#collectDependencies(RepositorySystemSession, CollectRequest)
+ */
+public final class CollectRequest
+{
+
+    private Artifact rootArtifact;
+
+    private Dependency root;
+
+    private List<Dependency> dependencies = Collections.emptyList();
+
+    private List<Dependency> managedDependencies = Collections.emptyList();
+
+    private List<RemoteRepository> repositories = Collections.emptyList();
+
+    private String context = "";
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public CollectRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request with the specified properties.
+     * 
+     * @param root The root dependency whose transitive dependencies should be collected, may be {@code null}.
+     * @param repositories The repositories to use for the collection, may be {@code null}.
+     */
+    public CollectRequest( Dependency root, List<RemoteRepository> repositories )
+    {
+        setRoot( root );
+        setRepositories( repositories );
+    }
+
+    /**
+     * Creates a new request with the specified properties.
+     * 
+     * @param root The root dependency whose transitive dependencies should be collected, may be {@code null}.
+     * @param dependencies The direct dependencies to merge with the direct dependencies from the root dependency's
+     *            artifact descriptor.
+     * @param repositories The repositories to use for the collection, may be {@code null}.
+     */
+    public CollectRequest( Dependency root, List<Dependency> dependencies, List<RemoteRepository> repositories )
+    {
+        setRoot( root );
+        setDependencies( dependencies );
+        setRepositories( repositories );
+    }
+
+    /**
+     * Creates a new request with the specified properties.
+     * 
+     * @param dependencies The direct dependencies of some imaginary root, may be {@code null}.
+     * @param managedDependencies The dependency management information to apply to the transitive dependencies, may be
+     *            {@code null}.
+     * @param repositories The repositories to use for the collection, may be {@code null}.
+     */
+    public CollectRequest( List<Dependency> dependencies, List<Dependency> managedDependencies,
+                           List<RemoteRepository> repositories )
+    {
+        setDependencies( dependencies );
+        setManagedDependencies( managedDependencies );
+        setRepositories( repositories );
+    }
+
+    /**
+     * Gets the root artifact for the dependency graph.
+     * 
+     * @return The root artifact for the dependency graph or {@code null} if none.
+     */
+    public Artifact getRootArtifact()
+    {
+        return rootArtifact;
+    }
+
+    /**
+     * Sets the root artifact for the dependency graph. This must not be confused with {@link #setRoot(Dependency)}: The
+     * root <em>dependency</em>, like any other specified dependency, will be subject to dependency
+     * collection/resolution, i.e. should have an artifact descriptor and a corresponding artifact file. The root
+     * <em>artifact</em> on the other hand is only used as a label for the root node of the graph in case no root
+     * dependency was specified. As such, the configured root artifact is ignored if {@link #getRoot()} does not return
+     * {@code null}.
+     * 
+     * @param rootArtifact The root artifact for the dependency graph, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest setRootArtifact( Artifact rootArtifact )
+    {
+        this.rootArtifact = rootArtifact;
+        return this;
+    }
+
+    /**
+     * Gets the root dependency of the graph.
+     * 
+     * @return The root dependency of the graph or {@code null} if none.
+     */
+    public Dependency getRoot()
+    {
+        return root;
+    }
+
+    /**
+     * Sets the root dependency of the graph.
+     * 
+     * @param root The root dependency of the graph, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest setRoot( Dependency root )
+    {
+        this.root = root;
+        return this;
+    }
+
+    /**
+     * Gets the direct dependencies.
+     * 
+     * @return The direct dependencies, never {@code null}.
+     */
+    public List<Dependency> getDependencies()
+    {
+        return dependencies;
+    }
+
+    /**
+     * Sets the direct dependencies. If both a root dependency and direct dependencies are given in the request, the
+     * direct dependencies from the request will be merged with the direct dependencies from the root dependency's
+     * artifact descriptor, giving higher priority to the dependencies from the request.
+     * 
+     * @param dependencies The direct dependencies, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest setDependencies( List<Dependency> dependencies )
+    {
+        if ( dependencies == null )
+        {
+            this.dependencies = Collections.emptyList();
+        }
+        else
+        {
+            this.dependencies = dependencies;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified direct dependency.
+     * 
+     * @param dependency The dependency to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest addDependency( Dependency dependency )
+    {
+        if ( dependency != null )
+        {
+            if ( this.dependencies.isEmpty() )
+            {
+                this.dependencies = new ArrayList<Dependency>();
+            }
+            this.dependencies.add( dependency );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the dependency management to apply to transitive dependencies.
+     * 
+     * @return The dependency management to apply to transitive dependencies, never {@code null}.
+     */
+    public List<Dependency> getManagedDependencies()
+    {
+        return managedDependencies;
+    }
+
+    /**
+     * Sets the dependency management to apply to transitive dependencies. To clarify, this management does not apply to
+     * the direct dependencies of the root node.
+     * 
+     * @param managedDependencies The dependency management, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest setManagedDependencies( List<Dependency> managedDependencies )
+    {
+        if ( managedDependencies == null )
+        {
+            this.managedDependencies = Collections.emptyList();
+        }
+        else
+        {
+            this.managedDependencies = managedDependencies;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified managed dependency.
+     * 
+     * @param managedDependency The managed dependency to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest addManagedDependency( Dependency managedDependency )
+    {
+        if ( managedDependency != null )
+        {
+            if ( this.managedDependencies.isEmpty() )
+            {
+                this.managedDependencies = new ArrayList<Dependency>();
+            }
+            this.managedDependencies.add( managedDependency );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the repositories to use for the collection.
+     * 
+     * @return The repositories to use for the collection, never {@code null}.
+     */
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the repositories to use for the collection.
+     * 
+     * @param repositories The repositories to use for the collection, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories == null )
+        {
+            this.repositories = Collections.emptyList();
+        }
+        else
+        {
+            this.repositories = repositories;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified repository for collection.
+     * 
+     * @param repository The repository to collect dependency information from, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest addRepository( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            if ( this.repositories.isEmpty() )
+            {
+                this.repositories = new ArrayList<RemoteRepository>();
+            }
+            this.repositories.add( repository );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the context in which this request is made.
+     * 
+     * @return The context, never {@code null}.
+     */
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the context in which this request is made.
+     * 
+     * @param context The context, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getRoot() + " -> " + getDependencies() + " < " + getRepositories();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/collection/CollectResult.java b/org.argeo.slc.repo/src/org/eclipse/aether/collection/CollectResult.java
new file mode 100644 (file)
index 0000000..4975190
--- /dev/null
@@ -0,0 +1,150 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.collection;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.graph.DependencyCycle;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * The result of a dependency collection request.
+ * 
+ * @see RepositorySystem#collectDependencies(RepositorySystemSession, CollectRequest)
+ */
+public final class CollectResult
+{
+
+    private final CollectRequest request;
+
+    private List<Exception> exceptions;
+
+    private List<DependencyCycle> cycles;
+
+    private DependencyNode root;
+
+    /**
+     * Creates a new result for the specified request.
+     * 
+     * @param request The resolution request, must not be {@code null}.
+     */
+    public CollectResult( CollectRequest request )
+    {
+        if ( request == null )
+        {
+            throw new IllegalArgumentException( "dependency collection request has not been specified" );
+        }
+        this.request = request;
+        exceptions = Collections.emptyList();
+        cycles = Collections.emptyList();
+    }
+
+    /**
+     * Gets the collection request that was made.
+     * 
+     * @return The collection request, never {@code null}.
+     */
+    public CollectRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the exceptions that occurred while building the dependency graph.
+     * 
+     * @return The exceptions that occurred, never {@code null}.
+     */
+    public List<Exception> getExceptions()
+    {
+        return exceptions;
+    }
+
+    /**
+     * Records the specified exception while building the dependency graph.
+     * 
+     * @param exception The exception to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public CollectResult addException( Exception exception )
+    {
+        if ( exception != null )
+        {
+            if ( exceptions.isEmpty() )
+            {
+                exceptions = new ArrayList<Exception>();
+            }
+            exceptions.add( exception );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the dependency cycles that were encountered while building the dependency graph.
+     * 
+     * @return The dependency cycles in the (raw) graph, never {@code null}.
+     */
+    public List<DependencyCycle> getCycles()
+    {
+        return cycles;
+    }
+
+    /**
+     * Records the specified dependency cycle.
+     * 
+     * @param cycle The dependency cycle to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public CollectResult addCycle( DependencyCycle cycle )
+    {
+        if ( cycle != null )
+        {
+            if ( cycles.isEmpty() )
+            {
+                cycles = new ArrayList<DependencyCycle>();
+            }
+            cycles.add( cycle );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the root node of the dependency graph.
+     * 
+     * @return The root node of the dependency graph or {@code null} if none.
+     */
+    public DependencyNode getRoot()
+    {
+        return root;
+    }
+
+    /**
+     * Sets the root node of the dependency graph.
+     * 
+     * @param root The root node of the dependency graph, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public CollectResult setRoot( DependencyNode root )
+    {
+        this.root = root;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( getRoot() );
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyCollectionContext.java b/org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyCollectionContext.java
new file mode 100644 (file)
index 0000000..3b8fbc2
--- /dev/null
@@ -0,0 +1,66 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.collection;
+
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * A context used during dependency collection to update the dependency manager, selector and traverser.
+ * 
+ * @see DependencyManager#deriveChildManager(DependencyCollectionContext)
+ * @see DependencyTraverser#deriveChildTraverser(DependencyCollectionContext)
+ * @see DependencySelector#deriveChildSelector(DependencyCollectionContext)
+ * @see VersionFilter#deriveChildFilter(DependencyCollectionContext)
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface DependencyCollectionContext
+{
+
+    /**
+     * Gets the repository system session during which the dependency collection happens.
+     * 
+     * @return The repository system session, never {@code null}.
+     */
+    RepositorySystemSession getSession();
+
+    /**
+     * Gets the artifact whose children are to be processed next during dependency collection. For all nodes but the
+     * root, this is simply shorthand for {@code getDependency().getArtifact()}. In case of the root node however,
+     * {@link #getDependency()} might be {@code null} while the node still has an artifact which serves as its label and
+     * is not to be resolved.
+     * 
+     * @return The artifact whose children are going to be processed or {@code null} in case of the root node without
+     *         dependency and label.
+     */
+    Artifact getArtifact();
+
+    /**
+     * Gets the dependency whose children are to be processed next during dependency collection.
+     * 
+     * @return The dependency whose children are going to be processed or {@code null} in case of the root node without
+     *         dependency.
+     */
+    Dependency getDependency();
+
+    /**
+     * Gets the dependency management information that was contributed by the artifact descriptor of the current
+     * dependency.
+     * 
+     * @return The dependency management information, never {@code null}.
+     */
+    List<Dependency> getManagedDependencies();
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyCollectionException.java b/org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyCollectionException.java
new file mode 100644 (file)
index 0000000..0d26674
--- /dev/null
@@ -0,0 +1,102 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.collection;
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of bad artifact descriptors, version ranges or other issues encountered during calculation of the
+ * dependency graph.
+ */
+public class DependencyCollectionException
+    extends RepositoryException
+{
+
+    private final transient CollectResult result;
+
+    /**
+     * Creates a new exception with the specified result.
+     * 
+     * @param result The collection result at the point the exception occurred, may be {@code null}.
+     */
+    public DependencyCollectionException( CollectResult result )
+    {
+        super( "Failed to collect dependencies for " + getSource( result ), getCause( result ) );
+        this.result = result;
+    }
+
+    /**
+     * Creates a new exception with the specified result and detail message.
+     * 
+     * @param result The collection result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public DependencyCollectionException( CollectResult result, String message )
+    {
+        super( message, getCause( result ) );
+        this.result = result;
+    }
+
+    /**
+     * Creates a new exception with the specified result, detail message and cause.
+     * 
+     * @param result The collection result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public DependencyCollectionException( CollectResult result, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.result = result;
+    }
+
+    /**
+     * Gets the collection result at the point the exception occurred. Despite being incomplete, callers might want to
+     * use this result to fail gracefully and continue their operation with whatever interim data has been gathered.
+     * 
+     * @return The collection result or {@code null} if unknown.
+     */
+    public CollectResult getResult()
+    {
+        return result;
+    }
+
+    private static String getSource( CollectResult result )
+    {
+        if ( result == null )
+        {
+            return "";
+        }
+
+        CollectRequest request = result.getRequest();
+        if ( request.getRoot() != null )
+        {
+            return request.getRoot().toString();
+        }
+        if ( request.getRootArtifact() != null )
+        {
+            return request.getRootArtifact().toString();
+        }
+
+        return request.getDependencies().toString();
+    }
+
+    private static Throwable getCause( CollectResult result )
+    {
+        Throwable cause = null;
+        if ( result != null && !result.getExceptions().isEmpty() )
+        {
+            cause = result.getExceptions().get( 0 );
+        }
+        return cause;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyGraphTransformationContext.java b/org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyGraphTransformationContext.java
new file mode 100644 (file)
index 0000000..d3980da
--- /dev/null
@@ -0,0 +1,49 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.collection;
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * A context used during dependency collection to exchange information within a chain of dependency graph transformers.
+ * 
+ * @see DependencyGraphTransformer
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface DependencyGraphTransformationContext
+{
+
+    /**
+     * Gets the repository system session during which the graph transformation happens.
+     * 
+     * @return The repository system session, never {@code null}.
+     */
+    RepositorySystemSession getSession();
+
+    /**
+     * Gets a keyed value from the context.
+     * 
+     * @param key The key used to query the value, must not be {@code null}.
+     * @return The queried value or {@code null} if none.
+     */
+    Object get( Object key );
+
+    /**
+     * Puts a keyed value into the context.
+     * 
+     * @param key The key used to store the value, must not be {@code null}.
+     * @param value The value to store, may be {@code null} to remove the mapping.
+     * @return The previous value associated with the key or {@code null} if none.
+     */
+    Object put( Object key, Object value );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyGraphTransformer.java b/org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyGraphTransformer.java
new file mode 100644 (file)
index 0000000..b3deebe
--- /dev/null
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.collection;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * Transforms a given dependency graph.
+ * <p>
+ * <strong>Note:</strong> Implementations must be stateless.
+ * <p>
+ * <em>Warning:</em> Dependency graphs may generally contain cycles. As such a graph transformer that cannot assume for
+ * sure that cycles have already been eliminated must gracefully handle cyclic graphs, e.g. guard against infinite
+ * recursion.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getDependencyGraphTransformer()
+ */
+public interface DependencyGraphTransformer
+{
+
+    /**
+     * Transforms the dependency graph denoted by the specified root node. The transformer may directly change the
+     * provided input graph or create a new graph, the former is recommended for performance reasons.
+     * 
+     * @param node The root node of the (possibly cyclic!) graph to transform, must not be {@code null}.
+     * @param context The graph transformation context, must not be {@code null}.
+     * @return The result graph of the transformation, never {@code null}.
+     * @throws RepositoryException If the transformation failed.
+     */
+    DependencyNode transformGraph( DependencyNode node, DependencyGraphTransformationContext context )
+        throws RepositoryException;
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyManagement.java b/org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyManagement.java
new file mode 100644 (file)
index 0000000..f0aac73
--- /dev/null
@@ -0,0 +1,168 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.collection;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.Exclusion;
+
+/**
+ * The management updates to apply to a dependency.
+ * 
+ * @see DependencyManager#manageDependency(Dependency)
+ */
+public final class DependencyManagement
+{
+
+    private String version;
+
+    private String scope;
+
+    private Boolean optional;
+
+    private Collection<Exclusion> exclusions;
+
+    private Map<String, String> properties;
+
+    /**
+     * Creates an empty management update.
+     */
+    public DependencyManagement()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Gets the new version to apply to the dependency.
+     * 
+     * @return The new version or {@code null} if the version is not managed and the existing dependency version should
+     *         remain unchanged.
+     */
+    public String getVersion()
+    {
+        return version;
+    }
+
+    /**
+     * Sets the new version to apply to the dependency.
+     * 
+     * @param version The new version, may be {@code null} if the version is not managed.
+     * @return This management update for chaining, never {@code null}.
+     */
+    public DependencyManagement setVersion( String version )
+    {
+        this.version = version;
+        return this;
+    }
+
+    /**
+     * Gets the new scope to apply to the dependency.
+     * 
+     * @return The new scope or {@code null} if the scope is not managed and the existing dependency scope should remain
+     *         unchanged.
+     */
+    public String getScope()
+    {
+        return scope;
+    }
+
+    /**
+     * Sets the new scope to apply to the dependency.
+     * 
+     * @param scope The new scope, may be {@code null} if the scope is not managed.
+     * @return This management update for chaining, never {@code null}.
+     */
+    public DependencyManagement setScope( String scope )
+    {
+        this.scope = scope;
+        return this;
+    }
+
+    /**
+     * Gets the new optional flag to apply to the dependency.
+     * 
+     * @return The new optional flag or {@code null} if the flag is not managed and the existing optional flag of the
+     *         dependency should remain unchanged.
+     */
+    public Boolean getOptional()
+    {
+        return optional;
+    }
+
+    /**
+     * Sets the new optional flag to apply to the dependency.
+     * 
+     * @param optional The optional flag, may be {@code null} if the flag is not managed.
+     * @return This management update for chaining, never {@code null}.
+     */
+    public DependencyManagement setOptional( Boolean optional )
+    {
+        this.optional = optional;
+        return this;
+    }
+
+    /**
+     * Gets the new exclusions to apply to the dependency. Note that this collection denotes the complete set of
+     * exclusions for the dependency, i.e. the dependency manager controls whether any existing exclusions get merged
+     * with information from dependency management or overridden by it.
+     * 
+     * @return The new exclusions or {@code null} if the exclusions are not managed and the existing dependency
+     *         exclusions should remain unchanged.
+     */
+    public Collection<Exclusion> getExclusions()
+    {
+        return exclusions;
+    }
+
+    /**
+     * Sets the new exclusions to apply to the dependency. Note that this collection denotes the complete set of
+     * exclusions for the dependency, i.e. the dependency manager controls whether any existing exclusions get merged
+     * with information from dependency management or overridden by it.
+     * 
+     * @param exclusions The new exclusions, may be {@code null} if the exclusions are not managed.
+     * @return This management update for chaining, never {@code null}.
+     */
+    public DependencyManagement setExclusions( Collection<Exclusion> exclusions )
+    {
+        this.exclusions = exclusions;
+        return this;
+    }
+
+    /**
+     * Gets the new properties to apply to the dependency. Note that this map denotes the complete set of properties,
+     * i.e. the dependency manager controls whether any existing properties get merged with the information from
+     * dependency management or overridden by it.
+     * 
+     * @return The new artifact properties or {@code null} if the properties are not managed and the existing properties
+     *         should remain unchanged.
+     */
+    public Map<String, String> getProperties()
+    {
+        return properties;
+    }
+
+    /**
+     * Sets the new properties to apply to the dependency. Note that this map denotes the complete set of properties,
+     * i.e. the dependency manager controls whether any existing properties get merged with the information from
+     * dependency management or overridden by it.
+     * 
+     * @param properties The new artifact properties, may be {@code null} if the properties are not managed.
+     * @return This management update for chaining, never {@code null}.
+     */
+    public DependencyManagement setProperties( Map<String, String> properties )
+    {
+        this.properties = properties;
+        return this;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyManager.java b/org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyManager.java
new file mode 100644 (file)
index 0000000..e214f66
--- /dev/null
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.collection;
+
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * Applies dependency management to the dependencies of a dependency node.
+ * <p>
+ * <strong>Note:</strong> Implementations must be stateless.
+ * <p>
+ * <em>Warning:</em> This hook is called from a hot spot and therefore implementations should pay attention to
+ * performance. Among others, implementations should provide a semantic {@link Object#equals(Object) equals()} method.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getDependencyManager()
+ * @see org.eclipse.aether.RepositorySystem#collectDependencies(org.eclipse.aether.RepositorySystemSession,
+ *      CollectRequest)
+ */
+public interface DependencyManager
+{
+
+    /**
+     * Applies dependency management to the specified dependency.
+     * 
+     * @param dependency The dependency to manage, must not be {@code null}.
+     * @return The management update to apply to the dependency or {@code null} if the dependency is not managed at all.
+     */
+    DependencyManagement manageDependency( Dependency dependency );
+
+    /**
+     * Derives a dependency manager for the specified collection context. When calculating the child manager,
+     * implementors are strongly advised to simply return the current instance if nothing changed to help save memory.
+     * 
+     * @param context The dependency collection context, must not be {@code null}.
+     * @return The dependency manager for the dependencies of the target node or {@code null} if dependency management
+     *         should no longer be applied.
+     */
+    DependencyManager deriveChildManager( DependencyCollectionContext context );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencySelector.java b/org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencySelector.java
new file mode 100644 (file)
index 0000000..de503be
--- /dev/null
@@ -0,0 +1,49 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.collection;
+
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * Decides what dependencies to include in the dependency graph.
+ * <p>
+ * <strong>Note:</strong> Implementations must be stateless.
+ * <p>
+ * <em>Warning:</em> This hook is called from a hot spot and therefore implementations should pay attention to
+ * performance. Among others, implementations should provide a semantic {@link Object#equals(Object) equals()} method.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getDependencySelector()
+ * @see org.eclipse.aether.RepositorySystem#collectDependencies(org.eclipse.aether.RepositorySystemSession,
+ *      CollectRequest)
+ */
+public interface DependencySelector
+{
+
+    /**
+     * Decides whether the specified dependency should be included in the dependency graph.
+     * 
+     * @param dependency The dependency to check, must not be {@code null}.
+     * @return {@code false} if the dependency should be excluded from the children of the current node, {@code true}
+     *         otherwise.
+     */
+    boolean selectDependency( Dependency dependency );
+
+    /**
+     * Derives a dependency selector for the specified collection context. When calculating the child selector,
+     * implementors are strongly advised to simply return the current instance if nothing changed to help save memory.
+     * 
+     * @param context The dependency collection context, must not be {@code null}.
+     * @return The dependency selector for the target node or {@code null} if dependencies should be unconditionally
+     *         included in the sub graph.
+     */
+    DependencySelector deriveChildSelector( DependencyCollectionContext context );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyTraverser.java b/org.argeo.slc.repo/src/org/eclipse/aether/collection/DependencyTraverser.java
new file mode 100644 (file)
index 0000000..8140395
--- /dev/null
@@ -0,0 +1,50 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.collection;
+
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * Decides whether the dependencies of a dependency node should be traversed as well.
+ * <p>
+ * <strong>Note:</strong> Implementations must be stateless.
+ * <p>
+ * <em>Warning:</em> This hook is called from a hot spot and therefore implementations should pay attention to
+ * performance. Among others, implementations should provide a semantic {@link Object#equals(Object) equals()} method.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getDependencyTraverser()
+ * @see org.eclipse.aether.RepositorySystem#collectDependencies(org.eclipse.aether.RepositorySystemSession,
+ *      CollectRequest)
+ */
+public interface DependencyTraverser
+{
+
+    /**
+     * Decides whether the dependencies of the specified dependency should be traversed.
+     * 
+     * @param dependency The dependency to check, must not be {@code null}.
+     * @return {@code true} if the dependency graph builder should recurse into the specified dependency and process its
+     *         dependencies, {@code false} otherwise.
+     */
+    boolean traverseDependency( Dependency dependency );
+
+    /**
+     * Derives a dependency traverser that will be used to decide whether the transitive dependencies of the dependency
+     * given in the collection context shall be traversed. When calculating the child traverser, implementors are
+     * strongly advised to simply return the current instance if nothing changed to help save memory.
+     * 
+     * @param context The dependency collection context, must not be {@code null}.
+     * @return The dependency traverser for the target node or {@code null} if dependencies should be unconditionally
+     *         traversed in the sub graph.
+     */
+    DependencyTraverser deriveChildTraverser( DependencyCollectionContext context );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/collection/UnsolvableVersionConflictException.java b/org.argeo.slc.repo/src/org/eclipse/aether/collection/UnsolvableVersionConflictException.java
new file mode 100644 (file)
index 0000000..8db5590
--- /dev/null
@@ -0,0 +1,133 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.collection;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.version.VersionConstraint;
+
+/**
+ * Thrown in case of an unsolvable conflict between different version constraints for a dependency.
+ */
+public class UnsolvableVersionConflictException
+    extends RepositoryException
+{
+
+    private final transient Collection<String> versions;
+
+    private final transient Collection<? extends List<? extends DependencyNode>> paths;
+
+    /**
+     * Creates a new exception with the specified paths to conflicting nodes in the dependency graph.
+     * 
+     * @param paths The paths to the dependency nodes that participate in the version conflict, may be {@code null}.
+     */
+    public UnsolvableVersionConflictException( Collection<? extends List<? extends DependencyNode>> paths )
+    {
+        super( "Could not resolve version conflict among " + toPaths( paths ) );
+        if ( paths == null )
+        {
+            this.paths = Collections.emptyList();
+            this.versions = Collections.emptyList();
+        }
+        else
+        {
+            this.paths = paths;
+            this.versions = new LinkedHashSet<String>();
+            for ( List<? extends DependencyNode> path : paths )
+            {
+                VersionConstraint constraint = path.get( path.size() - 1 ).getVersionConstraint();
+                if ( constraint != null && constraint.getRange() != null )
+                {
+                    versions.add( constraint.toString() );
+                }
+            }
+        }
+    }
+
+    private static String toPaths( Collection<? extends List<? extends DependencyNode>> paths )
+    {
+        String result = "";
+
+        if ( paths != null )
+        {
+            Collection<String> strings = new LinkedHashSet<String>();
+
+            for ( List<? extends DependencyNode> path : paths )
+            {
+                strings.add( toPath( path ) );
+            }
+
+            result = strings.toString();
+        }
+
+        return result;
+    }
+
+    private static String toPath( List<? extends DependencyNode> path )
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+
+        for ( Iterator<? extends DependencyNode> it = path.iterator(); it.hasNext(); )
+        {
+            DependencyNode node = it.next();
+            if ( node.getDependency() == null )
+            {
+                continue;
+            }
+
+            Artifact artifact = node.getDependency().getArtifact();
+            buffer.append( artifact.getGroupId() );
+            buffer.append( ':' ).append( artifact.getArtifactId() );
+            buffer.append( ':' ).append( artifact.getExtension() );
+            if ( artifact.getClassifier().length() > 0 )
+            {
+                buffer.append( ':' ).append( artifact.getClassifier() );
+            }
+            buffer.append( ':' ).append( node.getVersionConstraint() );
+
+            if ( it.hasNext() )
+            {
+                buffer.append( " -> " );
+            }
+        }
+
+        return buffer.toString();
+    }
+
+    /**
+     * Gets the paths leading to the conflicting dependencies.
+     * 
+     * @return The (read-only) paths leading to the conflicting dependencies, never {@code null}.
+     */
+    public Collection<? extends List<? extends DependencyNode>> getPaths()
+    {
+        return paths;
+    }
+
+    /**
+     * Gets the conflicting version constraints of the dependency.
+     * 
+     * @return The (read-only) conflicting version constraints, never {@code null}.
+     */
+    public Collection<String> getVersions()
+    {
+        return versions;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/collection/VersionFilter.java b/org.argeo.slc.repo/src/org/eclipse/aether/collection/VersionFilter.java
new file mode 100644 (file)
index 0000000..02e7ab3
--- /dev/null
@@ -0,0 +1,126 @@
+/*******************************************************************************
+ * Copyright (c) 2013, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.collection;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+
+/**
+ * Decides which versions matching a version range should actually be considered for the dependency graph. The version
+ * filter is not invoked for dependencies that do not declare a version range but a single version.
+ * <p>
+ * <strong>Note:</strong> Implementations must be stateless.
+ * <p>
+ * <em>Warning:</em> This hook is called from a hot spot and therefore implementations should pay attention to
+ * performance. Among others, implementations should provide a semantic {@link Object#equals(Object) equals()} method.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getVersionFilter()
+ * @see org.eclipse.aether.RepositorySystem#collectDependencies(org.eclipse.aether.RepositorySystemSession,
+ *      CollectRequest)
+ */
+public interface VersionFilter
+{
+
+    /**
+     * A context used during version filtering to hold relevant data.
+     * 
+     * @noimplement This interface is not intended to be implemented by clients.
+     * @noextend This interface is not intended to be extended by clients.
+     */
+    interface VersionFilterContext
+        extends Iterable<Version>
+    {
+
+        /**
+         * Gets the repository system session during which the version filtering happens.
+         * 
+         * @return The repository system session, never {@code null}.
+         */
+        RepositorySystemSession getSession();
+
+        /**
+         * Gets the dependency whose version range is being filtered.
+         * 
+         * @return The dependency, never {@code null}.
+         */
+        Dependency getDependency();
+
+        /**
+         * Gets the total number of available versions. This count reflects any removals made during version filtering.
+         * 
+         * @return The total number of available versions.
+         */
+        int getCount();
+
+        /**
+         * Gets an iterator over the available versions of the dependency. The iterator returns versions in ascending
+         * order. Use {@link Iterator#remove()} to exclude a version from further consideration in the dependency graph.
+         * 
+         * @return The iterator of available versions, never {@code null}.
+         */
+        Iterator<Version> iterator();
+
+        /**
+         * Gets the version constraint that was parsed from the dependency's version string.
+         * 
+         * @return The parsed version constraint, never {@code null}.
+         */
+        VersionConstraint getVersionConstraint();
+
+        /**
+         * Gets the repository from which the specified version was resolved.
+         * 
+         * @param version The version whose source repository should be retrieved, must not be {@code null}.
+         * @return The repository from which the version was resolved or {@code null} if unknown.
+         */
+        ArtifactRepository getRepository( Version version );
+
+        /**
+         * Gets the remote repositories from which the versions were resolved.
+         * 
+         * @return The (read-only) list of repositories, never {@code null}.
+         */
+        List<RemoteRepository> getRepositories();
+
+    }
+
+    /**
+     * Filters the available versions for a given dependency. Implementations will usually call
+     * {@link VersionFilterContext#iterator() context.iterator()} to inspect the available versions and use
+     * {@link java.util.Iterator#remove()} to delete unacceptable versions. If no versions remain after all filtering
+     * has been performed, the dependency collection process will automatically fail, i.e. implementations need not
+     * handle this situation on their own.
+     * 
+     * @param context The version filter context, must not be {@code null}.
+     * @throws RepositoryException If the filtering could not be performed.
+     */
+    void filterVersions( VersionFilterContext context )
+        throws RepositoryException;
+
+    /**
+     * Derives a version filter for the specified collection context. The derived filter will be used to handle version
+     * ranges encountered in child dependencies of the current node. When calculating the child filter, implementors are
+     * strongly advised to simply return the current instance if nothing changed to help save memory.
+     * 
+     * @param context The dependency collection context, must not be {@code null}.
+     * @return The version filter for the target node or {@code null} if versions should not be filtered any more.
+     */
+    VersionFilter deriveChildFilter( DependencyCollectionContext context );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/collection/package-info.java b/org.argeo.slc.repo/src/org/eclipse/aether/collection/package-info.java
new file mode 100644 (file)
index 0000000..dd7df2e
--- /dev/null
@@ -0,0 +1,16 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+/**
+ * The types and extension points for collecting the transitive dependencies of an artifact and building a dependency
+ * graph.
+ */
+package org.eclipse.aether.collection;
+
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/deployment/DeployRequest.java b/org.argeo.slc.repo/src/org/eclipse/aether/deployment/DeployRequest.java
new file mode 100644 (file)
index 0000000..a5372dd
--- /dev/null
@@ -0,0 +1,193 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.deployment;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A request to deploy artifacts and their accompanying metadata into the a remote repository.
+ * 
+ * @see RepositorySystem#deploy(RepositorySystemSession, DeployRequest)
+ */
+public final class DeployRequest
+{
+
+    private Collection<Artifact> artifacts = Collections.emptyList();
+
+    private Collection<Metadata> metadata = Collections.emptyList();
+
+    private RemoteRepository repository;
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public DeployRequest()
+    {
+    }
+
+    /**
+     * Gets the artifact to deploy.
+     * 
+     * @return The artifacts to deploy, never {@code null}.
+     */
+    public Collection<Artifact> getArtifacts()
+    {
+        return artifacts;
+    }
+
+    /**
+     * Sets the artifacts to deploy.
+     * 
+     * @param artifacts The artifacts to deploy, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DeployRequest setArtifacts( Collection<Artifact> artifacts )
+    {
+        if ( artifacts == null )
+        {
+            this.artifacts = Collections.emptyList();
+        }
+        else
+        {
+            this.artifacts = artifacts;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified artifacts for deployment.
+     * 
+     * @param artifact The artifact to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DeployRequest addArtifact( Artifact artifact )
+    {
+        if ( artifact != null )
+        {
+            if ( artifacts.isEmpty() )
+            {
+                artifacts = new ArrayList<Artifact>();
+            }
+            artifacts.add( artifact );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the metadata to deploy.
+     * 
+     * @return The metadata to deploy, never {@code null}.
+     */
+    public Collection<Metadata> getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Sets the metadata to deploy.
+     * 
+     * @param metadata The metadata to deploy, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DeployRequest setMetadata( Collection<Metadata> metadata )
+    {
+        if ( metadata == null )
+        {
+            this.metadata = Collections.emptyList();
+        }
+        else
+        {
+            this.metadata = metadata;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified metadata for deployment.
+     * 
+     * @param metadata The metadata to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DeployRequest addMetadata( Metadata metadata )
+    {
+        if ( metadata != null )
+        {
+            if ( this.metadata.isEmpty() )
+            {
+                this.metadata = new ArrayList<Metadata>();
+            }
+            this.metadata.add( metadata );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the repository to deploy to.
+     * 
+     * @return The repository to deploy to or {@code null} if not set.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the repository to deploy to.
+     * 
+     * @param repository The repository to deploy to, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DeployRequest setRepository( RemoteRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DeployRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifacts() + ", " + getMetadata() + " > " + getRepository();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/deployment/DeployResult.java b/org.argeo.slc.repo/src/org/eclipse/aether/deployment/DeployResult.java
new file mode 100644 (file)
index 0000000..fcda3ca
--- /dev/null
@@ -0,0 +1,165 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.deployment;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * The result of deploying artifacts and their accompanying metadata into the a remote repository.
+ * 
+ * @see RepositorySystem#deploy(RepositorySystemSession, DeployRequest)
+ */
+public final class DeployResult
+{
+
+    private final DeployRequest request;
+
+    private Collection<Artifact> artifacts;
+
+    private Collection<Metadata> metadata;
+
+    /**
+     * Creates a new result for the specified request.
+     * 
+     * @param request The deployment request, must not be {@code null}.
+     */
+    public DeployResult( DeployRequest request )
+    {
+        if ( request == null )
+        {
+            throw new IllegalArgumentException( "deploy request has not been specified" );
+        }
+        this.request = request;
+        artifacts = Collections.emptyList();
+        metadata = Collections.emptyList();
+    }
+
+    /**
+     * Gets the deploy request that was made.
+     * 
+     * @return The deploy request, never {@code null}.
+     */
+    public DeployRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the artifacts that got deployed.
+     * 
+     * @return The deployed artifacts, never {@code null}.
+     */
+    public Collection<Artifact> getArtifacts()
+    {
+        return artifacts;
+    }
+
+    /**
+     * Sets the artifacts that got deployed.
+     * 
+     * @param artifacts The deployed artifacts, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public DeployResult setArtifacts( Collection<Artifact> artifacts )
+    {
+        if ( artifacts == null )
+        {
+            this.artifacts = Collections.emptyList();
+        }
+        else
+        {
+            this.artifacts = artifacts;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified artifacts to the result.
+     * 
+     * @param artifact The deployed artifact to add, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public DeployResult addArtifact( Artifact artifact )
+    {
+        if ( artifact != null )
+        {
+            if ( artifacts.isEmpty() )
+            {
+                artifacts = new ArrayList<Artifact>();
+            }
+            artifacts.add( artifact );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the metadata that got deployed. Note that due to automatically generated metadata, there might have been
+     * more metadata deployed than originally specified in the deploy request.
+     * 
+     * @return The deployed metadata, never {@code null}.
+     */
+    public Collection<Metadata> getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Sets the metadata that got deployed.
+     * 
+     * @param metadata The deployed metadata, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public DeployResult setMetadata( Collection<Metadata> metadata )
+    {
+        if ( metadata == null )
+        {
+            this.metadata = Collections.emptyList();
+        }
+        else
+        {
+            this.metadata = metadata;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified metadata to this result.
+     * 
+     * @param metadata The deployed metadata to add, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public DeployResult addMetadata( Metadata metadata )
+    {
+        if ( metadata != null )
+        {
+            if ( this.metadata.isEmpty() )
+            {
+                this.metadata = new ArrayList<Metadata>();
+            }
+            this.metadata.add( metadata );
+        }
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifacts() + ", " + getMetadata();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/deployment/DeploymentException.java b/org.argeo.slc.repo/src/org/eclipse/aether/deployment/DeploymentException.java
new file mode 100644 (file)
index 0000000..f631530
--- /dev/null
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.deployment;
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of a deployment error like authentication failure.
+ */
+public class DeploymentException
+    extends RepositoryException
+{
+
+    /**
+     * Creates a new exception with the specified detail message.
+     * 
+     * @param message The detail message, may be {@code null}.
+     */
+    public DeploymentException( String message )
+    {
+        super( message );
+    }
+
+    /**
+     * Creates a new exception with the specified detail message and cause.
+     * 
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public DeploymentException( String message, Throwable cause )
+    {
+        super( message, cause );
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/deployment/package-info.java b/org.argeo.slc.repo/src/org/eclipse/aether/deployment/package-info.java
new file mode 100644 (file)
index 0000000..dd5a35d
--- /dev/null
@@ -0,0 +1,15 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+/**
+ * The types supporting the publishing of artifacts to a remote repository.
+ */
+package org.eclipse.aether.deployment;
+
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/graph/DefaultDependencyNode.java b/org.argeo.slc.repo/src/org/eclipse/aether/graph/DefaultDependencyNode.java
new file mode 100644 (file)
index 0000000..c702d23
--- /dev/null
@@ -0,0 +1,359 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.graph;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+
+/**
+ * A node within a dependency graph.
+ */
+public final class DefaultDependencyNode
+    implements DependencyNode
+{
+
+    private List<DependencyNode> children;
+
+    private Dependency dependency;
+
+    private Artifact artifact;
+
+    private List<? extends Artifact> relocations;
+
+    private Collection<? extends Artifact> aliases;
+
+    private VersionConstraint versionConstraint;
+
+    private Version version;
+
+    private byte managedBits;
+
+    private List<RemoteRepository> repositories;
+
+    private String context;
+
+    private Map<Object, Object> data;
+
+    /**
+     * Creates a new node with the specified dependency.
+     * 
+     * @param dependency The dependency associated with this node, may be {@code null} for a root node.
+     */
+    public DefaultDependencyNode( Dependency dependency )
+    {
+        this.dependency = dependency;
+        artifact = ( dependency != null ) ? dependency.getArtifact() : null;
+        children = new ArrayList<DependencyNode>( 0 );
+        aliases = relocations = Collections.emptyList();
+        repositories = Collections.emptyList();
+        context = "";
+        data = Collections.emptyMap();
+    }
+
+    /**
+     * Creates a new root node with the specified artifact as its label. Note that the new node has no dependency, i.e.
+     * {@link #getDependency()} will return {@code null}. Put differently, the specified artifact will not be subject to
+     * dependency collection/resolution.
+     * 
+     * @param artifact The artifact to use as label for this node, may be {@code null}.
+     */
+    public DefaultDependencyNode( Artifact artifact )
+    {
+        this.artifact = artifact;
+        children = new ArrayList<DependencyNode>( 0 );
+        aliases = relocations = Collections.emptyList();
+        repositories = Collections.emptyList();
+        context = "";
+        data = Collections.emptyMap();
+    }
+
+    /**
+     * Creates a mostly shallow clone of the specified node. The new node has its own copy of any custom data and
+     * initially no children.
+     * 
+     * @param node The node to copy, must not be {@code null}.
+     */
+    public DefaultDependencyNode( DependencyNode node )
+    {
+        dependency = node.getDependency();
+        artifact = node.getArtifact();
+        children = new ArrayList<DependencyNode>( 0 );
+        setAliases( node.getAliases() );
+        setRequestContext( node.getRequestContext() );
+        setManagedBits( node.getManagedBits() );
+        setRelocations( node.getRelocations() );
+        setRepositories( node.getRepositories() );
+        setVersion( node.getVersion() );
+        setVersionConstraint( node.getVersionConstraint() );
+        Map<?, ?> data = node.getData();
+        setData( data.isEmpty() ? null : new HashMap<Object, Object>( data ) );
+    }
+
+    public List<DependencyNode> getChildren()
+    {
+        return children;
+    }
+
+    public void setChildren( List<DependencyNode> children )
+    {
+        if ( children == null )
+        {
+            this.children = new ArrayList<DependencyNode>( 0 );
+        }
+        else
+        {
+            this.children = children;
+        }
+    }
+
+    public Dependency getDependency()
+    {
+        return dependency;
+    }
+
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    public void setArtifact( Artifact artifact )
+    {
+        if ( dependency == null )
+        {
+            throw new UnsupportedOperationException( "node does not have a dependency" );
+        }
+        dependency = dependency.setArtifact( artifact );
+        this.artifact = dependency.getArtifact();
+    }
+
+    public List<? extends Artifact> getRelocations()
+    {
+        return relocations;
+    }
+
+    /**
+     * Sets the sequence of relocations that was followed to resolve this dependency's artifact.
+     * 
+     * @param relocations The sequence of relocations, may be {@code null}.
+     */
+    public void setRelocations( List<? extends Artifact> relocations )
+    {
+        if ( relocations == null || relocations.isEmpty() )
+        {
+            this.relocations = Collections.emptyList();
+        }
+        else
+        {
+            this.relocations = relocations;
+        }
+    }
+
+    public Collection<? extends Artifact> getAliases()
+    {
+        return aliases;
+    }
+
+    /**
+     * Sets the known aliases for this dependency's artifact.
+     * 
+     * @param aliases The known aliases, may be {@code null}.
+     */
+    public void setAliases( Collection<? extends Artifact> aliases )
+    {
+        if ( aliases == null || aliases.isEmpty() )
+        {
+            this.aliases = Collections.emptyList();
+        }
+        else
+        {
+            this.aliases = aliases;
+        }
+    }
+
+    public VersionConstraint getVersionConstraint()
+    {
+        return versionConstraint;
+    }
+
+    /**
+     * Sets the version constraint that was parsed from the dependency's version declaration.
+     * 
+     * @param versionConstraint The version constraint for this node, may be {@code null}.
+     */
+    public void setVersionConstraint( VersionConstraint versionConstraint )
+    {
+        this.versionConstraint = versionConstraint;
+    }
+
+    public Version getVersion()
+    {
+        return version;
+    }
+
+    /**
+     * Sets the version that was selected for the dependency's target artifact.
+     * 
+     * @param version The parsed version, may be {@code null}.
+     */
+    public void setVersion( Version version )
+    {
+        this.version = version;
+    }
+
+    public void setScope( String scope )
+    {
+        if ( dependency == null )
+        {
+            throw new UnsupportedOperationException( "node does not have a dependency" );
+        }
+        dependency = dependency.setScope( scope );
+    }
+
+    public void setOptional( Boolean optional )
+    {
+        if ( dependency == null )
+        {
+            throw new UnsupportedOperationException( "node does not have a dependency" );
+        }
+        dependency = dependency.setOptional( optional );
+    }
+
+    public int getManagedBits()
+    {
+        return managedBits;
+    }
+
+    /**
+     * Sets a bit field indicating which attributes of this node were subject to dependency management.
+     * 
+     * @param managedBits The bit field indicating the managed attributes or {@code 0} if dependency management wasn't
+     *            applied.
+     */
+    public void setManagedBits( int managedBits )
+    {
+        this.managedBits = (byte) ( managedBits & 0x1F );
+    }
+
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the remote repositories from which this node's artifact shall be resolved.
+     * 
+     * @param repositories The remote repositories to use for artifact resolution, may be {@code null}.
+     */
+    public void setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories == null || repositories.isEmpty() )
+        {
+            this.repositories = Collections.emptyList();
+        }
+        else
+        {
+            this.repositories = repositories;
+        }
+    }
+
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    public void setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+    }
+
+    public Map<Object, Object> getData()
+    {
+        return data;
+    }
+
+    public void setData( Map<Object, Object> data )
+    {
+        if ( data == null )
+        {
+            this.data = Collections.emptyMap();
+        }
+        else
+        {
+            this.data = data;
+        }
+    }
+
+    public void setData( Object key, Object value )
+    {
+        if ( key == null )
+        {
+            throw new IllegalArgumentException( "key must not be null" );
+        }
+
+        if ( value == null )
+        {
+            if ( !data.isEmpty() )
+            {
+                data.remove( key );
+
+                if ( data.isEmpty() )
+                {
+                    data = Collections.emptyMap();
+                }
+            }
+        }
+        else
+        {
+            if ( data.isEmpty() )
+            {
+                data = new HashMap<Object, Object>( 1, 2 ); // nodes can be numerous so let's be space conservative
+            }
+            data.put( key, value );
+        }
+    }
+
+    public boolean accept( DependencyVisitor visitor )
+    {
+        if ( visitor.visitEnter( this ) )
+        {
+            for ( DependencyNode child : children )
+            {
+                if ( !child.accept( visitor ) )
+                {
+                    break;
+                }
+            }
+        }
+
+        return visitor.visitLeave( this );
+    }
+
+    @Override
+    public String toString()
+    {
+        Dependency dep = getDependency();
+        if ( dep == null )
+        {
+            return String.valueOf( getArtifact() );
+        }
+        return dep.toString();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/graph/Dependency.java b/org.argeo.slc.repo/src/org/eclipse/aether/graph/Dependency.java
new file mode 100644 (file)
index 0000000..72ea0f6
--- /dev/null
@@ -0,0 +1,321 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.graph;
+
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * A dependency to some artifact. <em>Note:</em> Instances of this class are immutable and the exposed mutators return
+ * new objects rather than changing the current instance.
+ */
+public final class Dependency
+{
+
+    private final Artifact artifact;
+
+    private final String scope;
+
+    private final Boolean optional;
+
+    private final Set<Exclusion> exclusions;
+
+    /**
+     * Creates a mandatory dependency on the specified artifact with the given scope.
+     * 
+     * @param artifact The artifact being depended on, must not be {@code null}.
+     * @param scope The scope of the dependency, may be {@code null}.
+     */
+    public Dependency( Artifact artifact, String scope )
+    {
+        this( artifact, scope, false );
+    }
+
+    /**
+     * Creates a dependency on the specified artifact with the given scope.
+     * 
+     * @param artifact The artifact being depended on, must not be {@code null}.
+     * @param scope The scope of the dependency, may be {@code null}.
+     * @param optional A flag whether the dependency is optional or mandatory, may be {@code null}.
+     */
+    public Dependency( Artifact artifact, String scope, Boolean optional )
+    {
+        this( artifact, scope, optional, null );
+    }
+
+    /**
+     * Creates a dependency on the specified artifact with the given scope and exclusions.
+     * 
+     * @param artifact The artifact being depended on, must not be {@code null}.
+     * @param scope The scope of the dependency, may be {@code null}.
+     * @param optional A flag whether the dependency is optional or mandatory, may be {@code null}.
+     * @param exclusions The exclusions that apply to transitive dependencies, may be {@code null} if none.
+     */
+    public Dependency( Artifact artifact, String scope, Boolean optional, Collection<Exclusion> exclusions )
+    {
+        this( artifact, scope, Exclusions.copy( exclusions ), optional );
+    }
+
+    private Dependency( Artifact artifact, String scope, Set<Exclusion> exclusions, Boolean optional )
+    {
+        // NOTE: This constructor assumes immutability of the provided exclusion collection, for internal use only
+        if ( artifact == null )
+        {
+            throw new IllegalArgumentException( "no artifact specified for dependency" );
+        }
+        this.artifact = artifact;
+        this.scope = ( scope != null ) ? scope : "";
+        this.optional = optional;
+        this.exclusions = exclusions;
+    }
+
+    /**
+     * Gets the artifact being depended on.
+     * 
+     * @return The artifact, never {@code null}.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact being depended on.
+     * 
+     * @param artifact The artifact, must not be {@code null}.
+     * @return The new dependency, never {@code null}.
+     */
+    public Dependency setArtifact( Artifact artifact )
+    {
+        if ( this.artifact.equals( artifact ) )
+        {
+            return this;
+        }
+        return new Dependency( artifact, scope, exclusions, optional );
+    }
+
+    /**
+     * Gets the scope of the dependency. The scope defines in which context this dependency is relevant.
+     * 
+     * @return The scope or an empty string if not set, never {@code null}.
+     */
+    public String getScope()
+    {
+        return scope;
+    }
+
+    /**
+     * Sets the scope of the dependency, e.g. "compile".
+     * 
+     * @param scope The scope of the dependency, may be {@code null}.
+     * @return The new dependency, never {@code null}.
+     */
+    public Dependency setScope( String scope )
+    {
+        if ( this.scope.equals( scope ) || ( scope == null && this.scope.length() <= 0 ) )
+        {
+            return this;
+        }
+        return new Dependency( artifact, scope, exclusions, optional );
+    }
+
+    /**
+     * Indicates whether this dependency is optional or not. Optional dependencies can be ignored in some contexts.
+     * 
+     * @return {@code true} if the dependency is (definitively) optional, {@code false} otherwise.
+     */
+    public boolean isOptional()
+    {
+        return Boolean.TRUE.equals( optional );
+    }
+
+    /**
+     * Gets the optional flag for the dependency. Note: Most clients will usually call {@link #isOptional()} to
+     * determine the optional flag, this method is for advanced use cases where three-valued logic is required.
+     * 
+     * @return The optional flag or {@code null} if unspecified.
+     */
+    public Boolean getOptional()
+    {
+        return optional;
+    }
+
+    /**
+     * Sets the optional flag for the dependency.
+     * 
+     * @param optional {@code true} if the dependency is optional, {@code false} if the dependency is mandatory, may be
+     *            {@code null} if unspecified.
+     * @return The new dependency, never {@code null}.
+     */
+    public Dependency setOptional( Boolean optional )
+    {
+        if ( eq( this.optional, optional ) )
+        {
+            return this;
+        }
+        return new Dependency( artifact, scope, exclusions, optional );
+    }
+
+    /**
+     * Gets the exclusions for this dependency. Exclusions can be used to remove transitive dependencies during
+     * resolution.
+     * 
+     * @return The (read-only) exclusions, never {@code null}.
+     */
+    public Collection<Exclusion> getExclusions()
+    {
+        return exclusions;
+    }
+
+    /**
+     * Sets the exclusions for the dependency.
+     * 
+     * @param exclusions The exclusions, may be {@code null}.
+     * @return The new dependency, never {@code null}.
+     */
+    public Dependency setExclusions( Collection<Exclusion> exclusions )
+    {
+        if ( hasEquivalentExclusions( exclusions ) )
+        {
+            return this;
+        }
+        return new Dependency( artifact, scope, optional, exclusions );
+    }
+
+    private boolean hasEquivalentExclusions( Collection<Exclusion> exclusions )
+    {
+        if ( exclusions == null || exclusions.isEmpty() )
+        {
+            return this.exclusions.isEmpty();
+        }
+        if ( exclusions instanceof Set )
+        {
+            return this.exclusions.equals( exclusions );
+        }
+        return exclusions.size() >= this.exclusions.size() && this.exclusions.containsAll( exclusions )
+            && exclusions.containsAll( this.exclusions );
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( getArtifact() ) + " (" + getScope() + ( isOptional() ? "?" : "" ) + ")";
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( obj == this )
+        {
+            return true;
+        }
+        else if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        Dependency that = (Dependency) obj;
+
+        return artifact.equals( that.artifact ) && scope.equals( that.scope ) && eq( optional, that.optional )
+            && exclusions.equals( that.exclusions );
+    }
+
+    private static <T> boolean eq( T o1, T o2 )
+    {
+        return ( o1 != null ) ? o1.equals( o2 ) : o2 == null;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + artifact.hashCode();
+        hash = hash * 31 + scope.hashCode();
+        hash = hash * 31 + ( optional != null ? optional.hashCode() : 0 );
+        hash = hash * 31 + exclusions.size();
+        return hash;
+    }
+
+    private static class Exclusions
+        extends AbstractSet<Exclusion>
+    {
+
+        private final Exclusion[] exclusions;
+
+        public static Set<Exclusion> copy( Collection<Exclusion> exclusions )
+        {
+            if ( exclusions == null || exclusions.isEmpty() )
+            {
+                return Collections.emptySet();
+            }
+            return new Exclusions( exclusions );
+        }
+
+        private Exclusions( Collection<Exclusion> exclusions )
+        {
+            if ( exclusions.size() > 1 && !( exclusions instanceof Set ) )
+            {
+                exclusions = new LinkedHashSet<Exclusion>( exclusions );
+            }
+            this.exclusions = exclusions.toArray( new Exclusion[exclusions.size()] );
+        }
+
+        @Override
+        public Iterator<Exclusion> iterator()
+        {
+            return new Iterator<Exclusion>()
+            {
+
+                private int cursor = 0;
+
+                public boolean hasNext()
+                {
+                    return cursor < exclusions.length;
+                }
+
+                public Exclusion next()
+                {
+                    try
+                    {
+                        Exclusion exclusion = exclusions[cursor];
+                        cursor++;
+                        return exclusion;
+                    }
+                    catch ( IndexOutOfBoundsException e )
+                    {
+                        throw new NoSuchElementException();
+                    }
+                }
+
+                public void remove()
+                {
+                    throw new UnsupportedOperationException();
+                }
+
+            };
+        }
+
+        @Override
+        public int size()
+        {
+            return exclusions.length;
+        }
+
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/graph/DependencyCycle.java b/org.argeo.slc.repo/src/org/eclipse/aether/graph/DependencyCycle.java
new file mode 100644 (file)
index 0000000..68f9a32
--- /dev/null
@@ -0,0 +1,44 @@
+/*******************************************************************************
+ * Copyright (c) 2013, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.graph;
+
+import java.util.List;
+
+/**
+ * A cycle within a dependency graph, that is a sequence of dependencies d_1, d_2, ..., d_n where d_1 and d_n have the
+ * same versionless coordinates. In more practical terms, a cycle occurs when a project directly or indirectly depends
+ * on its own output artifact.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface DependencyCycle
+{
+
+    /**
+     * Gets the dependencies that lead to the first dependency on the cycle, starting from the root of the dependency
+     * graph.
+     * 
+     * @return The (read-only) sequence of dependencies that precedes the cycle in the graph, potentially empty but
+     *         never {@code null}.
+     */
+    List<Dependency> getPrecedingDependencies();
+
+    /**
+     * Gets the dependencies that actually form the cycle. For example, a -&gt; b -&gt; c -&gt; a, i.e. the last
+     * dependency in this sequence duplicates the first element and closes the cycle. Hence the length of the cycle is
+     * the size of the returned sequence minus 1.
+     * 
+     * @return The (read-only) sequence of dependencies that forms the cycle, never {@code null}.
+     */
+    List<Dependency> getCyclicDependencies();
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/graph/DependencyFilter.java b/org.argeo.slc.repo/src/org/eclipse/aether/graph/DependencyFilter.java
new file mode 100644 (file)
index 0000000..c776ddc
--- /dev/null
@@ -0,0 +1,33 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.graph;
+
+import java.util.List;
+
+/**
+ * A filter to include/exclude dependency nodes during other operations.
+ */
+public interface DependencyFilter
+{
+
+    /**
+     * Indicates whether the specified dependency node shall be included or excluded.
+     * 
+     * @param node The dependency node to filter, must not be {@code null}.
+     * @param parents The (read-only) chain of parent nodes that leads to the node to be filtered, must not be
+     *            {@code null}. Iterating this (possibly empty) list walks up the dependency graph towards the root
+     *            node, i.e. the immediate parent node (if any) is the first node in the list. The size of the list also
+     *            denotes the zero-based depth of the filtered node.
+     * @return {@code true} to include the dependency node, {@code false} to exclude it.
+     */
+    boolean accept( DependencyNode node, List<DependencyNode> parents );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/graph/DependencyNode.java b/org.argeo.slc.repo/src/org/eclipse/aether/graph/DependencyNode.java
new file mode 100644 (file)
index 0000000..4e34597
--- /dev/null
@@ -0,0 +1,223 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.graph;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+
+/**
+ * A node within a dependency graph. To conserve memory, dependency graphs may reuse a given node instance multiple
+ * times to represent reoccurring dependencies. As such clients traversing a dependency graph should be prepared to
+ * discover multiple paths leading to the same node instance unless the input graph is known to be a duplicate-free
+ * tree. <em>Note:</em> Unless otherwise noted, implementation classes are not thread-safe and dependency nodes should
+ * not be mutated by concurrent threads.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface DependencyNode
+{
+
+    /**
+     * A bit flag indicating the dependency version was subject to dependency management
+     * 
+     * @see #getManagedBits()
+     */
+    int MANAGED_VERSION = 0x01;
+
+    /**
+     * A bit flag indicating the dependency scope was subject to dependency management
+     * 
+     * @see #getManagedBits()
+     */
+    int MANAGED_SCOPE = 0x02;
+
+    /**
+     * A bit flag indicating the optional flag was subject to dependency management
+     * 
+     * @see #getManagedBits()
+     */
+    int MANAGED_OPTIONAL = 0x04;
+
+    /**
+     * A bit flag indicating the artifact properties were subject to dependency management
+     * 
+     * @see #getManagedBits()
+     */
+    int MANAGED_PROPERTIES = 0x08;
+
+    /**
+     * A bit flag indicating the exclusions were subject to dependency management
+     * 
+     * @see #getManagedBits()
+     */
+    int MANAGED_EXCLUSIONS = 0x10;
+
+    /**
+     * Gets the child nodes of this node. To conserve memory, dependency nodes with equal dependencies may share the
+     * same child list instance. Hence clients mutating the child list need to be aware that these changes might affect
+     * more than this node. Where this is not desired, the child list should be copied before mutation if the client
+     * cannot be sure whether it might be shared with other nodes in the graph.
+     * 
+     * @return The child nodes of this node, never {@code null}.
+     */
+    List<DependencyNode> getChildren();
+
+    /**
+     * Sets the child nodes of this node.
+     * 
+     * @param children The child nodes, may be {@code null}
+     */
+    void setChildren( List<DependencyNode> children );
+
+    /**
+     * Gets the dependency associated with this node. <em>Note:</em> For dependency graphs that have been constructed
+     * without a root dependency, this method will yield {@code null} when invoked on the graph's root node. The root
+     * node of such graphs may however still have a label as returned by {@link #getArtifact()}.
+     * 
+     * @return The dependency or {@code null} if none.
+     */
+    Dependency getDependency();
+
+    /**
+     * Gets the artifact associated with this node. If this node is associated with a dependency, this is equivalent to
+     * {@code getDependency().getArtifact()}. Otherwise the artifact merely provides a label for this node in which case
+     * the artifact must not be subjected to dependency collection/resolution.
+     * 
+     * @return The associated artifact or {@code null} if none.
+     */
+    Artifact getArtifact();
+
+    /**
+     * Updates the artifact of the dependency after resolution. The new artifact must have the same coordinates as the
+     * original artifact. This method may only be invoked if this node actually has a dependency, i.e. if
+     * {@link #getDependency()} is not null.
+     * 
+     * @param artifact The artifact satisfying the dependency, must not be {@code null}.
+     */
+    void setArtifact( Artifact artifact );
+
+    /**
+     * Gets the sequence of relocations that was followed to resolve the artifact referenced by the dependency.
+     * 
+     * @return The (read-only) sequence of relocations, never {@code null}.
+     */
+    List<? extends Artifact> getRelocations();
+
+    /**
+     * Gets the known aliases for this dependency's artifact. An alias can be used to mark a patched rebuild of some
+     * other artifact as such, thereby allowing conflict resolution to consider the patched and the original artifact as
+     * a conflict.
+     * 
+     * @return The (read-only) set of known aliases, never {@code null}.
+     */
+    Collection<? extends Artifact> getAliases();
+
+    /**
+     * Gets the version constraint that was parsed from the dependency's version declaration.
+     * 
+     * @return The version constraint for this node or {@code null}.
+     */
+    VersionConstraint getVersionConstraint();
+
+    /**
+     * Gets the version that was selected for the dependency's target artifact.
+     * 
+     * @return The parsed version or {@code null}.
+     */
+    Version getVersion();
+
+    /**
+     * Sets the scope of the dependency. This method may only be invoked if this node actually has a dependency, i.e. if
+     * {@link #getDependency()} is not null.
+     * 
+     * @param scope The scope, may be {@code null}.
+     */
+    void setScope( String scope );
+
+    /**
+     * Sets the optional flag of the dependency. This method may only be invoked if this node actually has a dependency,
+     * i.e. if {@link #getDependency()} is not null.
+     * 
+     * @param optional The optional flag, may be {@code null}.
+     */
+    void setOptional( Boolean optional );
+
+    /**
+     * Gets a bit field indicating which attributes of this node were subject to dependency management.
+     * 
+     * @return A bit field containing any of the bits {@link #MANAGED_VERSION}, {@link #MANAGED_SCOPE},
+     *         {@link #MANAGED_OPTIONAL}, {@link #MANAGED_PROPERTIES} and {@link #MANAGED_EXCLUSIONS} if the
+     *         corresponding attribute was set via dependency management.
+     */
+    int getManagedBits();
+
+    /**
+     * Gets the remote repositories from which this node's artifact shall be resolved.
+     * 
+     * @return The (read-only) list of remote repositories to use for artifact resolution, never {@code null}.
+     */
+    List<RemoteRepository> getRepositories();
+
+    /**
+     * Gets the request context in which this dependency node was created.
+     * 
+     * @return The request context, never {@code null}.
+     */
+    String getRequestContext();
+
+    /**
+     * Sets the request context in which this dependency node was created.
+     * 
+     * @param context The context, may be {@code null}.
+     */
+    void setRequestContext( String context );
+
+    /**
+     * Gets the custom data associated with this dependency node. Clients of the repository system can use this data to
+     * annotate dependency nodes with domain-specific information. Note that the returned map is read-only and
+     * {@link #setData(Object, Object)} needs to be used to update the custom data.
+     * 
+     * @return The (read-only) key-value mappings, never {@code null}.
+     */
+    Map<?, ?> getData();
+
+    /**
+     * Sets the custom data associated with this dependency node.
+     * 
+     * @param data The new custom data, may be {@code null}.
+     */
+    void setData( Map<Object, Object> data );
+
+    /**
+     * Associates the specified dependency node data with the given key. <em>Note:</em> This method must not be called
+     * while {@link #getData()} is being iterated.
+     * 
+     * @param key The key under which to store the data, must not be {@code null}.
+     * @param value The data to associate with the key, may be {@code null} to remove the mapping.
+     */
+    void setData( Object key, Object value );
+
+    /**
+     * Traverses this node and potentially its children using the specified visitor.
+     * 
+     * @param visitor The visitor to call back, must not be {@code null}.
+     * @return {@code true} to visit siblings nodes of this node as well, {@code false} to skip siblings.
+     */
+    boolean accept( DependencyVisitor visitor );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/graph/DependencyVisitor.java b/org.argeo.slc.repo/src/org/eclipse/aether/graph/DependencyVisitor.java
new file mode 100644 (file)
index 0000000..d4ba213
--- /dev/null
@@ -0,0 +1,38 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.graph;
+
+/**
+ * A visitor for nodes of the dependency graph.
+ * 
+ * @see DependencyNode#accept(DependencyVisitor)
+ */
+public interface DependencyVisitor
+{
+
+    /**
+     * Notifies the visitor of a node visit before its children have been processed.
+     * 
+     * @param node The dependency node being visited, must not be {@code null}.
+     * @return {@code true} to visit child nodes of the specified node as well, {@code false} to skip children.
+     */
+    boolean visitEnter( DependencyNode node );
+
+    /**
+     * Notifies the visitor of a node visit after its children have been processed. Note that this method is always
+     * invoked regardless whether any children have actually been visited.
+     * 
+     * @param node The dependency node being visited, must not be {@code null}.
+     * @return {@code true} to visit siblings nodes of the specified node as well, {@code false} to skip siblings.
+     */
+    boolean visitLeave( DependencyNode node );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/graph/Exclusion.java b/org.argeo.slc.repo/src/org/eclipse/aether/graph/Exclusion.java
new file mode 100644 (file)
index 0000000..4d6b7ba
--- /dev/null
@@ -0,0 +1,122 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.graph;
+
+/**
+ * An exclusion of one or more transitive dependencies. <em>Note:</em> Instances of this class are immutable and the
+ * exposed mutators return new objects rather than changing the current instance.
+ * 
+ * @see Dependency#getExclusions()
+ */
+public final class Exclusion
+{
+
+    private final String groupId;
+
+    private final String artifactId;
+
+    private final String classifier;
+
+    private final String extension;
+
+    /**
+     * Creates an exclusion for artifacts with the specified coordinates.
+     * 
+     * @param groupId The group identifier, may be {@code null}.
+     * @param artifactId The artifact identifier, may be {@code null}.
+     * @param classifier The classifier, may be {@code null}.
+     * @param extension The file extension, may be {@code null}.
+     */
+    public Exclusion( String groupId, String artifactId, String classifier, String extension )
+    {
+        this.groupId = ( groupId != null ) ? groupId : "";
+        this.artifactId = ( artifactId != null ) ? artifactId : "";
+        this.classifier = ( classifier != null ) ? classifier : "";
+        this.extension = ( extension != null ) ? extension : "";
+    }
+
+    /**
+     * Gets the group identifier for artifacts to exclude.
+     * 
+     * @return The group identifier, never {@code null}.
+     */
+    public String getGroupId()
+    {
+        return groupId;
+    }
+
+    /**
+     * Gets the artifact identifier for artifacts to exclude.
+     * 
+     * @return The artifact identifier, never {@code null}.
+     */
+    public String getArtifactId()
+    {
+        return artifactId;
+    }
+
+    /**
+     * Gets the classifier for artifacts to exclude.
+     * 
+     * @return The classifier, never {@code null}.
+     */
+    public String getClassifier()
+    {
+        return classifier;
+    }
+
+    /**
+     * Gets the file extension for artifacts to exclude.
+     * 
+     * @return The file extension of artifacts to exclude, never {@code null}.
+     */
+    public String getExtension()
+    {
+        return extension;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getGroupId() + ':' + getArtifactId() + ':' + getExtension()
+            + ( getClassifier().length() > 0 ? ':' + getClassifier() : "" );
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( obj == this )
+        {
+            return true;
+        }
+        else if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        Exclusion that = (Exclusion) obj;
+
+        return artifactId.equals( that.artifactId ) && groupId.equals( that.groupId )
+            && extension.equals( that.extension ) && classifier.equals( that.classifier );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + artifactId.hashCode();
+        hash = hash * 31 + groupId.hashCode();
+        hash = hash * 31 + classifier.hashCode();
+        hash = hash * 31 + extension.hashCode();
+        return hash;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/graph/package-info.java b/org.argeo.slc.repo/src/org/eclipse/aether/graph/package-info.java
new file mode 100644 (file)
index 0000000..70879a3
--- /dev/null
@@ -0,0 +1,15 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+/**
+ * The representation of a dependency graph by means of connected dependency nodes.
+ */
+package org.eclipse.aether.graph;
+
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/installation/InstallRequest.java b/org.argeo.slc.repo/src/org/eclipse/aether/installation/InstallRequest.java
new file mode 100644 (file)
index 0000000..330f85a
--- /dev/null
@@ -0,0 +1,168 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.installation;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * A request to install artifacts and their accompanying metadata into the local repository.
+ * 
+ * @see RepositorySystem#install(RepositorySystemSession, InstallRequest)
+ */
+public final class InstallRequest
+{
+
+    private Collection<Artifact> artifacts = Collections.emptyList();
+
+    private Collection<Metadata> metadata = Collections.emptyList();
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public InstallRequest()
+    {
+    }
+
+    /**
+     * Gets the artifact to install.
+     * 
+     * @return The artifacts to install, never {@code null}.
+     */
+    public Collection<Artifact> getArtifacts()
+    {
+        return artifacts;
+    }
+
+    /**
+     * Sets the artifacts to install.
+     * 
+     * @param artifacts The artifacts to install, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public InstallRequest setArtifacts( Collection<Artifact> artifacts )
+    {
+        if ( artifacts == null )
+        {
+            this.artifacts = Collections.emptyList();
+        }
+        else
+        {
+            this.artifacts = artifacts;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified artifacts for installation.
+     * 
+     * @param artifact The artifact to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public InstallRequest addArtifact( Artifact artifact )
+    {
+        if ( artifact != null )
+        {
+            if ( artifacts.isEmpty() )
+            {
+                artifacts = new ArrayList<Artifact>();
+            }
+            artifacts.add( artifact );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the metadata to install.
+     * 
+     * @return The metadata to install, never {@code null}.
+     */
+    public Collection<Metadata> getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Sets the metadata to install.
+     * 
+     * @param metadata The metadata to install.
+     * @return This request for chaining, never {@code null}.
+     */
+    public InstallRequest setMetadata( Collection<Metadata> metadata )
+    {
+        if ( metadata == null )
+        {
+            this.metadata = Collections.emptyList();
+        }
+        else
+        {
+            this.metadata = metadata;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified metadata for installation.
+     * 
+     * @param metadata The metadata to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public InstallRequest addMetadata( Metadata metadata )
+    {
+        if ( metadata != null )
+        {
+            if ( this.metadata.isEmpty() )
+            {
+                this.metadata = new ArrayList<Metadata>();
+            }
+            this.metadata.add( metadata );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public InstallRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifacts() + ", " + getMetadata();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/installation/InstallResult.java b/org.argeo.slc.repo/src/org/eclipse/aether/installation/InstallResult.java
new file mode 100644 (file)
index 0000000..fe3ade1
--- /dev/null
@@ -0,0 +1,165 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.installation;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * The result of installing artifacts and their accompanying metadata into the a remote repository.
+ * 
+ * @see RepositorySystem#install(RepositorySystemSession, InstallRequest)
+ */
+public final class InstallResult
+{
+
+    private final InstallRequest request;
+
+    private Collection<Artifact> artifacts;
+
+    private Collection<Metadata> metadata;
+
+    /**
+     * Creates a new result for the specified request.
+     * 
+     * @param request The installation request, must not be {@code null}.
+     */
+    public InstallResult( InstallRequest request )
+    {
+        if ( request == null )
+        {
+            throw new IllegalArgumentException( "install request has not been specified" );
+        }
+        this.request = request;
+        artifacts = Collections.emptyList();
+        metadata = Collections.emptyList();
+    }
+
+    /**
+     * Gets the install request that was made.
+     * 
+     * @return The install request, never {@code null}.
+     */
+    public InstallRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the artifacts that got installed.
+     * 
+     * @return The installed artifacts, never {@code null}.
+     */
+    public Collection<Artifact> getArtifacts()
+    {
+        return artifacts;
+    }
+
+    /**
+     * Sets the artifacts that got installed.
+     * 
+     * @param artifacts The installed artifacts, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public InstallResult setArtifacts( Collection<Artifact> artifacts )
+    {
+        if ( artifacts == null )
+        {
+            this.artifacts = Collections.emptyList();
+        }
+        else
+        {
+            this.artifacts = artifacts;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified artifacts to the result.
+     * 
+     * @param artifact The installed artifact to add, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public InstallResult addArtifact( Artifact artifact )
+    {
+        if ( artifact != null )
+        {
+            if ( artifacts.isEmpty() )
+            {
+                artifacts = new ArrayList<Artifact>();
+            }
+            artifacts.add( artifact );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the metadata that got installed. Note that due to automatically generated metadata, there might have been
+     * more metadata installed than originally specified in the install request.
+     * 
+     * @return The installed metadata, never {@code null}.
+     */
+    public Collection<Metadata> getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Sets the metadata that got installed.
+     * 
+     * @param metadata The installed metadata, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public InstallResult setMetadata( Collection<Metadata> metadata )
+    {
+        if ( metadata == null )
+        {
+            this.metadata = Collections.emptyList();
+        }
+        else
+        {
+            this.metadata = metadata;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified metadata to this result.
+     * 
+     * @param metadata The installed metadata to add, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public InstallResult addMetadata( Metadata metadata )
+    {
+        if ( metadata != null )
+        {
+            if ( this.metadata.isEmpty() )
+            {
+                this.metadata = new ArrayList<Metadata>();
+            }
+            this.metadata.add( metadata );
+        }
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifacts() + ", " + getMetadata();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/installation/InstallationException.java b/org.argeo.slc.repo/src/org/eclipse/aether/installation/InstallationException.java
new file mode 100644 (file)
index 0000000..e976665
--- /dev/null
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.installation;
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of an installation error like an IO error.
+ */
+public class InstallationException
+    extends RepositoryException
+{
+
+    /**
+     * Creates a new exception with the specified detail message.
+     * 
+     * @param message The detail message, may be {@code null}.
+     */
+    public InstallationException( String message )
+    {
+        super( message );
+    }
+
+    /**
+     * Creates a new exception with the specified detail message and cause.
+     * 
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public InstallationException( String message, Throwable cause )
+    {
+        super( message, cause );
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/installation/package-info.java b/org.argeo.slc.repo/src/org/eclipse/aether/installation/package-info.java
new file mode 100644 (file)
index 0000000..35b910b
--- /dev/null
@@ -0,0 +1,15 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+/**
+ * The types supporting the publishing of artifacts to a local repository.
+ */
+package org.eclipse.aether.installation;
+
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/metadata/AbstractMetadata.java b/org.argeo.slc.repo/src/org/eclipse/aether/metadata/AbstractMetadata.java
new file mode 100644 (file)
index 0000000..d95eb54
--- /dev/null
@@ -0,0 +1,151 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.metadata;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A skeleton class for metadata.
+ */
+public abstract class AbstractMetadata
+    implements Metadata
+{
+
+    private Metadata newInstance( Map<String, String> properties, File file )
+    {
+        return new DefaultMetadata( getGroupId(), getArtifactId(), getVersion(), getType(), getNature(), file,
+                                    properties );
+    }
+
+    public Metadata setFile( File file )
+    {
+        File current = getFile();
+        if ( ( current == null ) ? file == null : current.equals( file ) )
+        {
+            return this;
+        }
+        return newInstance( getProperties(), file );
+    }
+
+    public Metadata setProperties( Map<String, String> properties )
+    {
+        Map<String, String> current = getProperties();
+        if ( current.equals( properties ) || ( properties == null && current.isEmpty() ) )
+        {
+            return this;
+        }
+        return newInstance( copyProperties( properties ), getFile() );
+    }
+
+    public String getProperty( String key, String defaultValue )
+    {
+        String value = getProperties().get( key );
+        return ( value != null ) ? value : defaultValue;
+    }
+
+    /**
+     * Copies the specified metadata properties. This utility method should be used when creating new metadata instances
+     * with caller-supplied properties.
+     * 
+     * @param properties The properties to copy, may be {@code null}.
+     * @return The copied and read-only properties, never {@code null}.
+     */
+    protected static Map<String, String> copyProperties( Map<String, String> properties )
+    {
+        if ( properties != null && !properties.isEmpty() )
+        {
+            return Collections.unmodifiableMap( new HashMap<String, String>( properties ) );
+        }
+        else
+        {
+            return Collections.emptyMap();
+        }
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder buffer = new StringBuilder( 128 );
+        if ( getGroupId().length() > 0 )
+        {
+            buffer.append( getGroupId() );
+        }
+        if ( getArtifactId().length() > 0 )
+        {
+            buffer.append( ':' ).append( getArtifactId() );
+        }
+        if ( getVersion().length() > 0 )
+        {
+            buffer.append( ':' ).append( getVersion() );
+        }
+        buffer.append( '/' ).append( getType() );
+        return buffer.toString();
+    }
+
+    /**
+     * Compares this metadata with the specified object.
+     * 
+     * @param obj The object to compare this metadata against, may be {@code null}.
+     * @return {@code true} if and only if the specified object is another {@link Metadata} with equal coordinates,
+     *         type, nature, properties and file, {@code false} otherwise.
+     */
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( obj == this )
+        {
+            return true;
+        }
+        else if ( !( obj instanceof Metadata ) )
+        {
+            return false;
+        }
+
+        Metadata that = (Metadata) obj;
+
+        return getArtifactId().equals( that.getArtifactId() ) && getGroupId().equals( that.getGroupId() )
+            && getVersion().equals( that.getVersion() ) && getType().equals( that.getType() )
+            && getNature().equals( that.getNature() ) && eq( getFile(), that.getFile() )
+            && eq( getProperties(), that.getProperties() );
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    /**
+     * Returns a hash code for this metadata.
+     * 
+     * @return A hash code for the metadata.
+     */
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + getGroupId().hashCode();
+        hash = hash * 31 + getArtifactId().hashCode();
+        hash = hash * 31 + getType().hashCode();
+        hash = hash * 31 + getNature().hashCode();
+        hash = hash * 31 + getVersion().hashCode();
+        hash = hash * 31 + hash( getFile() );
+        return hash;
+    }
+
+    private static int hash( Object obj )
+    {
+        return ( obj != null ) ? obj.hashCode() : 0;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/metadata/DefaultMetadata.java b/org.argeo.slc.repo/src/org/eclipse/aether/metadata/DefaultMetadata.java
new file mode 100644 (file)
index 0000000..aa9c830
--- /dev/null
@@ -0,0 +1,183 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.metadata;
+
+import java.io.File;
+import java.util.Map;
+
+/**
+ * A basic metadata instance. <em>Note:</em> Instances of this class are immutable and the exposed mutators return new
+ * objects rather than changing the current instance.
+ */
+public final class DefaultMetadata
+    extends AbstractMetadata
+{
+
+    private final String groupId;
+
+    private final String artifactId;
+
+    private final String version;
+
+    private final String type;
+
+    private final Nature nature;
+
+    private final File file;
+
+    private final Map<String, String> properties;
+
+    /**
+     * Creates a new metadata for the repository root with the specific type and nature.
+     * 
+     * @param type The type of the metadata, e.g. "maven-metadata.xml", may be {@code null}.
+     * @param nature The nature of the metadata, must not be {@code null}.
+     */
+    public DefaultMetadata( String type, Nature nature )
+    {
+        this( "", "", "", type, nature, null, (File) null );
+    }
+
+    /**
+     * Creates a new metadata for the groupId level with the specific type and nature.
+     * 
+     * @param groupId The group identifier to which this metadata applies, may be {@code null}.
+     * @param type The type of the metadata, e.g. "maven-metadata.xml", may be {@code null}.
+     * @param nature The nature of the metadata, must not be {@code null}.
+     */
+    public DefaultMetadata( String groupId, String type, Nature nature )
+    {
+        this( groupId, "", "", type, nature, null, (File) null );
+    }
+
+    /**
+     * Creates a new metadata for the groupId:artifactId level with the specific type and nature.
+     * 
+     * @param groupId The group identifier to which this metadata applies, may be {@code null}.
+     * @param artifactId The artifact identifier to which this metadata applies, may be {@code null}.
+     * @param type The type of the metadata, e.g. "maven-metadata.xml", may be {@code null}.
+     * @param nature The nature of the metadata, must not be {@code null}.
+     */
+    public DefaultMetadata( String groupId, String artifactId, String type, Nature nature )
+    {
+        this( groupId, artifactId, "", type, nature, null, (File) null );
+    }
+
+    /**
+     * Creates a new metadata for the groupId:artifactId:version level with the specific type and nature.
+     * 
+     * @param groupId The group identifier to which this metadata applies, may be {@code null}.
+     * @param artifactId The artifact identifier to which this metadata applies, may be {@code null}.
+     * @param version The version to which this metadata applies, may be {@code null}.
+     * @param type The type of the metadata, e.g. "maven-metadata.xml", may be {@code null}.
+     * @param nature The nature of the metadata, must not be {@code null}.
+     */
+    public DefaultMetadata( String groupId, String artifactId, String version, String type, Nature nature )
+    {
+        this( groupId, artifactId, version, type, nature, null, (File) null );
+    }
+
+    /**
+     * Creates a new metadata for the groupId:artifactId:version level with the specific type and nature.
+     * 
+     * @param groupId The group identifier to which this metadata applies, may be {@code null}.
+     * @param artifactId The artifact identifier to which this metadata applies, may be {@code null}.
+     * @param version The version to which this metadata applies, may be {@code null}.
+     * @param type The type of the metadata, e.g. "maven-metadata.xml", may be {@code null}.
+     * @param nature The nature of the metadata, must not be {@code null}.
+     * @param file The resolved file of the metadata, may be {@code null}.
+     */
+    public DefaultMetadata( String groupId, String artifactId, String version, String type, Nature nature, File file )
+    {
+        this( groupId, artifactId, version, type, nature, null, file );
+    }
+
+    /**
+     * Creates a new metadata for the groupId:artifactId:version level with the specific type and nature.
+     * 
+     * @param groupId The group identifier to which this metadata applies, may be {@code null}.
+     * @param artifactId The artifact identifier to which this metadata applies, may be {@code null}.
+     * @param version The version to which this metadata applies, may be {@code null}.
+     * @param type The type of the metadata, e.g. "maven-metadata.xml", may be {@code null}.
+     * @param nature The nature of the metadata, must not be {@code null}.
+     * @param properties The properties of the metadata, may be {@code null} if none.
+     * @param file The resolved file of the metadata, may be {@code null}.
+     */
+    public DefaultMetadata( String groupId, String artifactId, String version, String type, Nature nature,
+                            Map<String, String> properties, File file )
+    {
+        this.groupId = emptify( groupId );
+        this.artifactId = emptify( artifactId );
+        this.version = emptify( version );
+        this.type = emptify( type );
+        if ( nature == null )
+        {
+            throw new IllegalArgumentException( "metadata nature was not specified" );
+        }
+        this.nature = nature;
+        this.file = file;
+        this.properties = copyProperties( properties );
+    }
+
+    DefaultMetadata( String groupId, String artifactId, String version, String type, Nature nature, File file,
+                     Map<String, String> properties )
+    {
+        // NOTE: This constructor assumes immutability of the provided properties, for internal use only
+        this.groupId = emptify( groupId );
+        this.artifactId = emptify( artifactId );
+        this.version = emptify( version );
+        this.type = emptify( type );
+        this.nature = nature;
+        this.file = file;
+        this.properties = properties;
+    }
+
+    private static String emptify( String str )
+    {
+        return ( str == null ) ? "" : str;
+    }
+
+    public String getGroupId()
+    {
+        return groupId;
+    }
+
+    public String getArtifactId()
+    {
+        return artifactId;
+    }
+
+    public String getVersion()
+    {
+        return version;
+    }
+
+    public String getType()
+    {
+        return type;
+    }
+
+    public Nature getNature()
+    {
+        return nature;
+    }
+
+    public File getFile()
+    {
+        return file;
+    }
+
+    public Map<String, String> getProperties()
+    {
+        return properties;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/metadata/MergeableMetadata.java b/org.argeo.slc.repo/src/org/eclipse/aether/metadata/MergeableMetadata.java
new file mode 100644 (file)
index 0000000..25f15df
--- /dev/null
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.metadata;
+
+import java.io.File;
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * A piece of metadata that needs to be merged with any current metadata before installation/deployment.
+ */
+public interface MergeableMetadata
+    extends Metadata
+{
+
+    /**
+     * Merges this metadata into the current metadata (if any). Note that this method will be invoked regardless whether
+     * metadata currently exists or not.
+     * 
+     * @param current The path to the current metadata file, may not exist but must not be {@code null}.
+     * @param result The path to the result file where the merged metadata should be stored, must not be {@code null}.
+     * @throws RepositoryException If the metadata could not be merged.
+     */
+    void merge( File current, File result )
+        throws RepositoryException;
+
+    /**
+     * Indicates whether this metadata has been merged.
+     * 
+     * @return {@code true} if the metadata has been merged, {@code false} otherwise.
+     */
+    boolean isMerged();
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/metadata/Metadata.java b/org.argeo.slc.repo/src/org/eclipse/aether/metadata/Metadata.java
new file mode 100644 (file)
index 0000000..328544a
--- /dev/null
@@ -0,0 +1,129 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.metadata;
+
+import java.io.File;
+import java.util.Map;
+
+/**
+ * A piece of repository metadata, e.g. an index of available versions. In contrast to an artifact, which usually exists
+ * in only one repository, metadata usually exists in multiple repositories and each repository contains a different
+ * copy of the metadata. <em>Note:</em> Metadata instances are supposed to be immutable, e.g. any exposed mutator method
+ * returns a new metadata instance and leaves the original instance unchanged. Implementors are strongly advised to obey
+ * this contract. <em>Note:</em> Implementors are strongly advised to inherit from {@link AbstractMetadata} instead of
+ * directly implementing this interface.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface Metadata
+{
+
+    /**
+     * The nature of the metadata.
+     */
+    enum Nature
+    {
+        /**
+         * The metadata refers to release artifacts only.
+         */
+        RELEASE,
+
+        /**
+         * The metadata refers to snapshot artifacts only.
+         */
+        SNAPSHOT,
+
+        /**
+         * The metadata refers to either release or snapshot artifacts.
+         */
+        RELEASE_OR_SNAPSHOT
+    }
+
+    /**
+     * Gets the group identifier of this metadata.
+     * 
+     * @return The group identifier or an empty string if the metadata applies to the entire repository, never
+     *         {@code null}.
+     */
+    String getGroupId();
+
+    /**
+     * Gets the artifact identifier of this metadata.
+     * 
+     * @return The artifact identifier or an empty string if the metadata applies to the groupId level only, never
+     *         {@code null}.
+     */
+    String getArtifactId();
+
+    /**
+     * Gets the version of this metadata.
+     * 
+     * @return The version or an empty string if the metadata applies to the groupId:artifactId level only, never
+     *         {@code null}.
+     */
+    String getVersion();
+
+    /**
+     * Gets the type of the metadata, e.g. "maven-metadata.xml".
+     * 
+     * @return The type of the metadata, never {@code null}.
+     */
+    String getType();
+
+    /**
+     * Gets the nature of this metadata. The nature indicates to what artifact versions the metadata refers.
+     * 
+     * @return The nature, never {@code null}.
+     */
+    Nature getNature();
+
+    /**
+     * Gets the file of this metadata. Note that only resolved metadata has a file associated with it.
+     * 
+     * @return The file or {@code null} if none.
+     */
+    File getFile();
+
+    /**
+     * Sets the file of the metadata.
+     * 
+     * @param file The file of the metadata, may be {@code null}
+     * @return The new metadata, never {@code null}.
+     */
+    Metadata setFile( File file );
+
+    /**
+     * Gets the specified property.
+     * 
+     * @param key The name of the property, must not be {@code null}.
+     * @param defaultValue The default value to return in case the property is not set, may be {@code null}.
+     * @return The requested property value or {@code null} if the property is not set and no default value was
+     *         provided.
+     */
+    String getProperty( String key, String defaultValue );
+
+    /**
+     * Gets the properties of this metadata.
+     * 
+     * @return The (read-only) properties, never {@code null}.
+     */
+    Map<String, String> getProperties();
+
+    /**
+     * Sets the properties for the metadata.
+     * 
+     * @param properties The properties for the metadata, may be {@code null}.
+     * @return The new metadata, never {@code null}.
+     */
+    Metadata setProperties( Map<String, String> properties );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/metadata/package-info.java b/org.argeo.slc.repo/src/org/eclipse/aether/metadata/package-info.java
new file mode 100644 (file)
index 0000000..141a837
--- /dev/null
@@ -0,0 +1,15 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+/**
+ * The definition of metadata, that is an auxiliary entity managed by the repository system to locate artifacts.
+ */
+package org.eclipse.aether.metadata;
+
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/package-info.java b/org.argeo.slc.repo/src/org/eclipse/aether/package-info.java
new file mode 100644 (file)
index 0000000..7268b46
--- /dev/null
@@ -0,0 +1,15 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+/**
+ * The primary API of the {@link org.eclipse.aether.RepositorySystem} and its functionality.
+ */
+package org.eclipse.aether;
+
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/ArtifactRepository.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/ArtifactRepository.java
new file mode 100644 (file)
index 0000000..2000f8b
--- /dev/null
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+/**
+ * A repository hosting artifacts.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface ArtifactRepository
+{
+
+    /**
+     * Gets the type of the repository, for example "default".
+     * 
+     * @return The (case-sensitive) type of the repository, never {@code null}.
+     */
+    String getContentType();
+
+    /**
+     * Gets the identifier of this repository.
+     * 
+     * @return The (case-sensitive) identifier, never {@code null}.
+     */
+    String getId();
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/Authentication.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/Authentication.java
new file mode 100644 (file)
index 0000000..c1eaac0
--- /dev/null
@@ -0,0 +1,46 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+import java.util.Map;
+
+/**
+ * The authentication to use for accessing a protected resource. This acts basically as an extensible callback mechanism
+ * from which network operations can request authentication data like username and password when needed.
+ */
+public interface Authentication
+{
+
+    /**
+     * Fills the given authentication context with the data from this authentication callback. To do so, implementors
+     * have to call {@link AuthenticationContext#put(String, Object)}. <br>
+     * <br>
+     * The {@code key} parameter supplied to this method acts merely as a hint for interactive callbacks that want to
+     * prompt the user for only that authentication data which is required. Implementations are free to ignore this
+     * parameter and put all the data they have into the authentication context at once.
+     * 
+     * @param context The authentication context to populate, must not be {@code null}.
+     * @param key The key denoting a specific piece of authentication data that is being requested for a network
+     *            operation, may be {@code null}.
+     * @param data Any (read-only) extra data in form of key value pairs that might be useful when getting the
+     *            authentication data, may be {@code null}.
+     */
+    void fill( AuthenticationContext context, String key, Map<String, String> data );
+
+    /**
+     * Updates the given digest with data from this authentication callback. To do so, implementors have to call the
+     * {@code update()} methods in {@link AuthenticationDigest}.
+     * 
+     * @param digest The digest to update, must not be {@code null}.
+     */
+    void digest( AuthenticationDigest digest );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/AuthenticationContext.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/AuthenticationContext.java
new file mode 100644 (file)
index 0000000..5b1ba2c
--- /dev/null
@@ -0,0 +1,380 @@
+/*******************************************************************************
+ * Copyright (c) 2012, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+import java.io.Closeable;
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * A glorified map of key value pairs holding (cleartext) authentication data. Authentication contexts are used
+ * internally when network operations need to access secured repositories or proxies. Each authentication context
+ * manages the credentials required to access a single host. Unlike {@link Authentication} callbacks which exist for a
+ * potentially long time like the duration of a repository system session, an authentication context has a supposedly
+ * short lifetime and should be {@link #close() closed} as soon as the corresponding network operation has finished:
+ * 
+ * <pre>
+ * AuthenticationContext context = AuthenticationContext.forRepository( session, repository );
+ * try {
+ *     // get credentials
+ *     char[] password = context.get( AuthenticationContext.PASSWORD, char[].class );
+ *     // perform network operation using retrieved credentials
+ *     ...
+ * } finally {
+ *     // erase confidential authentication data from heap memory
+ *     AuthenticationContext.close( context );
+ * }
+ * </pre>
+ * 
+ * The same authentication data can often be presented using different data types, e.g. a password can be presented
+ * using a character array or (less securely) using a string. For ease of use, an authentication context treats the
+ * following groups of data types as equivalent and converts values automatically during retrieval:
+ * <ul>
+ * <li>{@code String}, {@code char[]}</li>
+ * <li>{@code String}, {@code File}</li>
+ * </ul>
+ * An authentication context is thread-safe.
+ */
+public final class AuthenticationContext
+    implements Closeable
+{
+
+    /**
+     * The key used to store the username. The corresponding authentication data should be of type {@link String}.
+     */
+    public static final String USERNAME = "username";
+
+    /**
+     * The key used to store the password. The corresponding authentication data should be of type {@code char[]} or
+     * {@link String}.
+     */
+    public static final String PASSWORD = "password";
+
+    /**
+     * The key used to store the NTLM domain. The corresponding authentication data should be of type {@link String}.
+     */
+    public static final String NTLM_DOMAIN = "ntlm.domain";
+
+    /**
+     * The key used to store the NTML workstation. The corresponding authentication data should be of type
+     * {@link String}.
+     */
+    public static final String NTLM_WORKSTATION = "ntlm.workstation";
+
+    /**
+     * The key used to store the pathname to a private key file. The corresponding authentication data should be of type
+     * {@link String} or {@link File}.
+     */
+    public static final String PRIVATE_KEY_PATH = "privateKey.path";
+
+    /**
+     * The key used to store the passphrase protecting the private key. The corresponding authentication data should be
+     * of type {@code char[]} or {@link String}.
+     */
+    public static final String PRIVATE_KEY_PASSPHRASE = "privateKey.passphrase";
+
+    /**
+     * The key used to store the acceptance policy for unknown host keys. The corresponding authentication data should
+     * be of type {@link Boolean}. When querying this authentication data, the extra data should provide
+     * {@link #HOST_KEY_REMOTE} and {@link #HOST_KEY_LOCAL}, e.g. to enable a well-founded decision of the user during
+     * an interactive prompt.
+     */
+    public static final String HOST_KEY_ACCEPTANCE = "hostKey.acceptance";
+
+    /**
+     * The key used to store the fingerprint of the public key advertised by remote host. Note that this key is used to
+     * query the extra data passed to {@link #get(String, Map, Class)} when getting {@link #HOST_KEY_ACCEPTANCE}, not
+     * the authentication data in a context.
+     */
+    public static final String HOST_KEY_REMOTE = "hostKey.remote";
+
+    /**
+     * The key used to store the fingerprint of the public key expected from remote host as recorded in a known hosts
+     * database. Note that this key is used to query the extra data passed to {@link #get(String, Map, Class)} when
+     * getting {@link #HOST_KEY_ACCEPTANCE}, not the authentication data in a context.
+     */
+    public static final String HOST_KEY_LOCAL = "hostKey.local";
+
+    /**
+     * The key used to store the SSL context. The corresponding authentication data should be of type
+     * {@link javax.net.ssl.SSLContext}.
+     */
+    public static final String SSL_CONTEXT = "ssl.context";
+
+    /**
+     * The key used to store the SSL hostname verifier. The corresponding authentication data should be of type
+     * {@link javax.net.ssl.HostnameVerifier}.
+     */
+    public static final String SSL_HOSTNAME_VERIFIER = "ssl.hostnameVerifier";
+
+    private final RepositorySystemSession session;
+
+    private final RemoteRepository repository;
+
+    private final Proxy proxy;
+
+    private final Authentication auth;
+
+    private final Map<String, Object> authData;
+
+    private boolean fillingAuthData;
+
+    /**
+     * Gets an authentication context for the specified repository.
+     * 
+     * @param session The repository system session during which the repository is accessed, must not be {@code null}.
+     * @param repository The repository for which to create an authentication context, must not be {@code null}.
+     * @return An authentication context for the repository or {@code null} if no authentication is configured for it.
+     */
+    public static AuthenticationContext forRepository( RepositorySystemSession session, RemoteRepository repository )
+    {
+        return newInstance( session, repository, null, repository.getAuthentication() );
+    }
+
+    /**
+     * Gets an authentication context for the proxy of the specified repository.
+     * 
+     * @param session The repository system session during which the repository is accessed, must not be {@code null}.
+     * @param repository The repository for whose proxy to create an authentication context, must not be {@code null}.
+     * @return An authentication context for the proxy or {@code null} if no proxy is set or no authentication is
+     *         configured for it.
+     */
+    public static AuthenticationContext forProxy( RepositorySystemSession session, RemoteRepository repository )
+    {
+        Proxy proxy = repository.getProxy();
+        return newInstance( session, repository, proxy, ( proxy != null ) ? proxy.getAuthentication() : null );
+    }
+
+    private static AuthenticationContext newInstance( RepositorySystemSession session, RemoteRepository repository,
+                                                      Proxy proxy, Authentication auth )
+    {
+        if ( auth == null )
+        {
+            return null;
+        }
+        return new AuthenticationContext( session, repository, proxy, auth );
+    }
+
+    private AuthenticationContext( RepositorySystemSession session, RemoteRepository repository, Proxy proxy,
+                                   Authentication auth )
+    {
+        if ( session == null )
+        {
+            throw new IllegalArgumentException( "repository system session missing" );
+        }
+        this.session = session;
+        this.repository = repository;
+        this.proxy = proxy;
+        this.auth = auth;
+        authData = new HashMap<String, Object>();
+    }
+
+    /**
+     * Gets the repository system session during which the authentication happens.
+     * 
+     * @return The repository system session, never {@code null}.
+     */
+    public RepositorySystemSession getSession()
+    {
+        return session;
+    }
+
+    /**
+     * Gets the repository requiring authentication. If {@link #getProxy()} is not {@code null}, the data gathered by
+     * this authentication context does not apply to the repository's host but rather the proxy.
+     * 
+     * @return The repository to be contacted, never {@code null}.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Gets the proxy (if any) to be authenticated with.
+     * 
+     * @return The proxy or {@code null} if authenticating directly with the repository's host.
+     */
+    public Proxy getProxy()
+    {
+        return proxy;
+    }
+
+    /**
+     * Gets the authentication data for the specified key.
+     * 
+     * @param key The key whose authentication data should be retrieved, must not be {@code null}.
+     * @return The requested authentication data or {@code null} if none.
+     */
+    public String get( String key )
+    {
+        return get( key, null, String.class );
+    }
+
+    /**
+     * Gets the authentication data for the specified key.
+     * 
+     * @param <T> The data type of the authentication data.
+     * @param key The key whose authentication data should be retrieved, must not be {@code null}.
+     * @param type The expected type of the authentication data, must not be {@code null}.
+     * @return The requested authentication data or {@code null} if none or if the data doesn't match the expected type.
+     */
+    public <T> T get( String key, Class<T> type )
+    {
+        return get( key, null, type );
+    }
+
+    /**
+     * Gets the authentication data for the specified key.
+     * 
+     * @param <T> The data type of the authentication data.
+     * @param key The key whose authentication data should be retrieved, must not be {@code null}.
+     * @param data Any (read-only) extra data in form of key value pairs that might be useful when getting the
+     *            authentication data, may be {@code null}.
+     * @param type The expected type of the authentication data, must not be {@code null}.
+     * @return The requested authentication data or {@code null} if none or if the data doesn't match the expected type.
+     */
+    public <T> T get( String key, Map<String, String> data, Class<T> type )
+    {
+        if ( key == null )
+        {
+            throw new IllegalArgumentException( "authentication data key missing" );
+        }
+        Object value;
+        synchronized ( authData )
+        {
+            value = authData.get( key );
+            if ( value == null && !authData.containsKey( key ) && !fillingAuthData )
+            {
+                if ( auth != null )
+                {
+                    try
+                    {
+                        fillingAuthData = true;
+                        auth.fill( this, key, data );
+                    }
+                    finally
+                    {
+                        fillingAuthData = false;
+                    }
+                    value = authData.get( key );
+                }
+                if ( value == null )
+                {
+                    authData.put( key, value );
+                }
+            }
+        }
+
+        return convert( value, type );
+    }
+
+    private <T> T convert( Object value, Class<T> type )
+    {
+        if ( !type.isInstance( value ) )
+        {
+            if ( String.class.equals( type ) )
+            {
+                if ( value instanceof File )
+                {
+                    value = ( (File) value ).getPath();
+                }
+                else if ( value instanceof char[] )
+                {
+                    value = new String( (char[]) value );
+                }
+            }
+            else if ( File.class.equals( type ) )
+            {
+                if ( value instanceof String )
+                {
+                    value = new File( (String) value );
+                }
+            }
+            else if ( char[].class.equals( type ) )
+            {
+                if ( value instanceof String )
+                {
+                    value = ( (String) value ).toCharArray();
+                }
+            }
+        }
+
+        if ( type.isInstance( value ) )
+        {
+            return type.cast( value );
+        }
+
+        return null;
+    }
+
+    /**
+     * Puts the specified authentication data into this context. This method should only be called from implementors of
+     * {@link Authentication#fill(AuthenticationContext, String, Map)}. Passed in character arrays are not cloned and
+     * become owned by this context, i.e. get erased when the context gets closed.
+     * 
+     * @param key The key to associate the authentication data with, must not be {@code null}.
+     * @param value The (cleartext) authentication data to store, may be {@code null}.
+     */
+    public void put( String key, Object value )
+    {
+        if ( key == null )
+        {
+            throw new IllegalArgumentException( "authentication data key missing" );
+        }
+        synchronized ( authData )
+        {
+            Object oldValue = authData.put( key, value );
+            if ( oldValue instanceof char[] )
+            {
+                Arrays.fill( (char[]) oldValue, '\0' );
+            }
+        }
+    }
+
+    /**
+     * Closes this authentication context and erases sensitive authentication data from heap memory. Closing an already
+     * closed context has no effect.
+     */
+    public void close()
+    {
+        synchronized ( authData )
+        {
+            for ( Object value : authData.values() )
+            {
+                if ( value instanceof char[] )
+                {
+                    Arrays.fill( (char[]) value, '\0' );
+                }
+            }
+            authData.clear();
+        }
+    }
+
+    /**
+     * Closes the specified authentication context. This is a convenience method doing a {@code null} check before
+     * calling {@link #close()} on the given context.
+     * 
+     * @param context The authentication context to close, may be {@code null}.
+     */
+    public static void close( AuthenticationContext context )
+    {
+        if ( context != null )
+        {
+            context.close();
+        }
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/AuthenticationDigest.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/AuthenticationDigest.java
new file mode 100644 (file)
index 0000000..f702b4a
--- /dev/null
@@ -0,0 +1,210 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * A helper to calculate a fingerprint/digest for the authentication data of a repository/proxy. Such a fingerprint can
+ * be used to detect changes in the authentication data across JVM restarts without exposing sensitive information.
+ */
+public final class AuthenticationDigest
+{
+
+    private final MessageDigest digest;
+
+    private final RepositorySystemSession session;
+
+    private final RemoteRepository repository;
+
+    private final Proxy proxy;
+
+    /**
+     * Gets the fingerprint for the authentication of the specified repository.
+     * 
+     * @param session The repository system session during which the fingerprint is requested, must not be {@code null}.
+     * @param repository The repository whose authentication is to be fingerprinted, must not be {@code null}.
+     * @return The fingerprint of the repository authentication or an empty string if no authentication is configured,
+     *         never {@code null}.
+     */
+    public static String forRepository( RepositorySystemSession session, RemoteRepository repository )
+    {
+        String digest = "";
+        Authentication auth = repository.getAuthentication();
+        if ( auth != null )
+        {
+            AuthenticationDigest authDigest = new AuthenticationDigest( session, repository, null );
+            auth.digest( authDigest );
+            digest = authDigest.digest();
+        }
+        return digest;
+    }
+
+    /**
+     * Gets the fingerprint for the authentication of the specified repository's proxy.
+     * 
+     * @param session The repository system session during which the fingerprint is requested, must not be {@code null}.
+     * @param repository The repository whose proxy authentication is to be fingerprinted, must not be {@code null}.
+     * @return The fingerprint of the proxy authentication or an empty string if no proxy is present or if no proxy
+     *         authentication is configured, never {@code null}.
+     */
+    public static String forProxy( RepositorySystemSession session, RemoteRepository repository )
+    {
+        String digest = "";
+        Proxy proxy = repository.getProxy();
+        if ( proxy != null )
+        {
+            Authentication auth = proxy.getAuthentication();
+            if ( auth != null )
+            {
+                AuthenticationDigest authDigest = new AuthenticationDigest( session, repository, proxy );
+                auth.digest( authDigest );
+                digest = authDigest.digest();
+            }
+        }
+        return digest;
+    }
+
+    private AuthenticationDigest( RepositorySystemSession session, RemoteRepository repository, Proxy proxy )
+    {
+        this.session = session;
+        this.repository = repository;
+        this.proxy = proxy;
+        digest = newDigest();
+    }
+
+    private static MessageDigest newDigest()
+    {
+        try
+        {
+            return MessageDigest.getInstance( "SHA-1" );
+        }
+        catch ( NoSuchAlgorithmException e )
+        {
+            try
+            {
+                return MessageDigest.getInstance( "MD5" );
+            }
+            catch ( NoSuchAlgorithmException ne )
+            {
+                throw new IllegalStateException( ne );
+            }
+        }
+    }
+
+    /**
+     * Gets the repository system session during which the authentication fingerprint is calculated.
+     * 
+     * @return The repository system session, never {@code null}.
+     */
+    public RepositorySystemSession getSession()
+    {
+        return session;
+    }
+
+    /**
+     * Gets the repository requiring authentication. If {@link #getProxy()} is not {@code null}, the data gathered by
+     * this authentication digest does not apply to the repository's host but rather the proxy.
+     * 
+     * @return The repository to be contacted, never {@code null}.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Gets the proxy (if any) to be authenticated with.
+     * 
+     * @return The proxy or {@code null} if authenticating directly with the repository's host.
+     */
+    public Proxy getProxy()
+    {
+        return proxy;
+    }
+
+    /**
+     * Updates the digest with the specified strings.
+     * 
+     * @param strings The strings to update the digest with, may be {@code null} or contain {@code null} elements.
+     */
+    public void update( String... strings )
+    {
+        if ( strings != null )
+        {
+            for ( String string : strings )
+            {
+                if ( string != null )
+                {
+                    try
+                    {
+                        digest.update( string.getBytes( "UTF-8" ) );
+                    }
+                    catch ( UnsupportedEncodingException e )
+                    {
+                        throw new IllegalStateException( e );
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Updates the digest with the specified characters.
+     * 
+     * @param chars The characters to update the digest with, may be {@code null}.
+     */
+    public void update( char... chars )
+    {
+        if ( chars != null )
+        {
+            for ( char c : chars )
+            {
+                digest.update( (byte) ( c >> 8 ) );
+                digest.update( (byte) ( c & 0xFF ) );
+            }
+        }
+    }
+
+    /**
+     * Updates the digest with the specified bytes.
+     * 
+     * @param bytes The bytes to update the digest with, may be {@code null}.
+     */
+    public void update( byte... bytes )
+    {
+        if ( bytes != null )
+        {
+            digest.update( bytes );
+        }
+    }
+
+    private String digest()
+    {
+        byte[] bytes = digest.digest();
+        StringBuilder buffer = new StringBuilder( bytes.length * 2 );
+        for ( byte aByte : bytes )
+        {
+            int b = aByte & 0xFF;
+            if ( b < 0x10 )
+            {
+                buffer.append( '0' );
+            }
+            buffer.append( Integer.toHexString( b ) );
+        }
+        return buffer.toString();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/AuthenticationSelector.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/AuthenticationSelector.java
new file mode 100644 (file)
index 0000000..46c9bab
--- /dev/null
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+/**
+ * Selects authentication for a given remote repository.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getAuthenticationSelector()
+ */
+public interface AuthenticationSelector
+{
+
+    /**
+     * Selects authentication for the specified remote repository.
+     * 
+     * @param repository The repository for which to select authentication, must not be {@code null}.
+     * @return The selected authentication or {@code null} if none.
+     */
+    Authentication getAuthentication( RemoteRepository repository );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalArtifactRegistration.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalArtifactRegistration.java
new file mode 100644 (file)
index 0000000..af6ea4e
--- /dev/null
@@ -0,0 +1,140 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * A request to register an artifact within the local repository. Certain local repository implementations can refuse to
+ * serve physically present artifacts if those haven't been previously registered to them.
+ * 
+ * @see LocalRepositoryManager#add(RepositorySystemSession, LocalArtifactRegistration)
+ */
+public final class LocalArtifactRegistration
+{
+
+    private Artifact artifact;
+
+    private RemoteRepository repository;
+
+    private Collection<String> contexts = Collections.emptyList();
+
+    /**
+     * Creates an uninitialized registration.
+     */
+    public LocalArtifactRegistration()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a registration request for the specified (locally installed) artifact.
+     * 
+     * @param artifact The artifact to register, may be {@code null}.
+     */
+    public LocalArtifactRegistration( Artifact artifact )
+    {
+        setArtifact( artifact );
+    }
+
+    /**
+     * Creates a registration request for the specified artifact.
+     * 
+     * @param artifact The artifact to register, may be {@code null}.
+     * @param repository The remote repository from which the artifact was resolved or {@code null} if the artifact was
+     *            locally installed.
+     * @param contexts The resolution contexts, may be {@code null}.
+     */
+    public LocalArtifactRegistration( Artifact artifact, RemoteRepository repository, Collection<String> contexts )
+    {
+        setArtifact( artifact );
+        setRepository( repository );
+        setContexts( contexts );
+    }
+
+    /**
+     * Gets the artifact to register.
+     * 
+     * @return The artifact or {@code null} if not set.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact to register.
+     * 
+     * @param artifact The artifact, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public LocalArtifactRegistration setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the remote repository from which the artifact was resolved.
+     * 
+     * @return The remote repository or {@code null} if the artifact was locally installed.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the remote repository from which the artifact was resolved.
+     * 
+     * @param repository The remote repository or {@code null} if the artifact was locally installed.
+     * @return This request for chaining, never {@code null}.
+     */
+    public LocalArtifactRegistration setRepository( RemoteRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    /**
+     * Gets the resolution contexts in which the artifact is available.
+     * 
+     * @return The resolution contexts in which the artifact is available, never {@code null}.
+     */
+    public Collection<String> getContexts()
+    {
+        return contexts;
+    }
+
+    /**
+     * Sets the resolution contexts in which the artifact is available.
+     * 
+     * @param contexts The resolution contexts, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public LocalArtifactRegistration setContexts( Collection<String> contexts )
+    {
+        if ( contexts != null )
+        {
+            this.contexts = contexts;
+        }
+        else
+        {
+            this.contexts = Collections.emptyList();
+        }
+        return this;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalArtifactRequest.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalArtifactRequest.java
new file mode 100644 (file)
index 0000000..3cc67f8
--- /dev/null
@@ -0,0 +1,136 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * A query to the local repository for the existence of an artifact.
+ * 
+ * @see LocalRepositoryManager#find(RepositorySystemSession, LocalArtifactRequest)
+ */
+public final class LocalArtifactRequest
+{
+
+    private Artifact artifact;
+
+    private String context = "";
+
+    private List<RemoteRepository> repositories = Collections.emptyList();
+
+    /**
+     * Creates an uninitialized query.
+     */
+    public LocalArtifactRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a query with the specified properties.
+     * 
+     * @param artifact The artifact to query for, may be {@code null}.
+     * @param repositories The remote repositories that should be considered as potential sources for the artifact, may
+     *            be {@code null} or empty to only consider locally installed artifacts.
+     * @param context The resolution context for the artifact, may be {@code null}.
+     */
+    public LocalArtifactRequest( Artifact artifact, List<RemoteRepository> repositories, String context )
+    {
+        setArtifact( artifact );
+        setRepositories( repositories );
+        setContext( context );
+    }
+
+    /**
+     * Gets the artifact to query for.
+     * 
+     * @return The artifact or {@code null} if not set.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact to query for.
+     * 
+     * @param artifact The artifact, may be {@code null}.
+     * @return This query for chaining, never {@code null}.
+     */
+    public LocalArtifactRequest setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the resolution context.
+     * 
+     * @return The resolution context, never {@code null}.
+     */
+    public String getContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the resolution context.
+     * 
+     * @param context The resolution context, may be {@code null}.
+     * @return This query for chaining, never {@code null}.
+     */
+    public LocalArtifactRequest setContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Gets the remote repositories to consider as sources of the artifact.
+     * 
+     * @return The remote repositories, never {@code null}.
+     */
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the remote repositories to consider as sources of the artifact.
+     * 
+     * @param repositories The remote repositories, may be {@code null} or empty to only consider locally installed
+     *            artifacts.
+     * @return This query for chaining, never {@code null}.
+     */
+    public LocalArtifactRequest setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories != null )
+        {
+            this.repositories = repositories;
+        }
+        else
+        {
+            this.repositories = Collections.emptyList();
+        }
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifact() + " @ " + getRepositories();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalArtifactResult.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalArtifactResult.java
new file mode 100644 (file)
index 0000000..065b823
--- /dev/null
@@ -0,0 +1,138 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+import java.io.File;
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * A result from the local repository about the existence of an artifact.
+ * 
+ * @see LocalRepositoryManager#find(RepositorySystemSession, LocalArtifactRequest)
+ */
+public final class LocalArtifactResult
+{
+
+    private final LocalArtifactRequest request;
+
+    private File file;
+
+    private boolean available;
+
+    private RemoteRepository repository;
+
+    /**
+     * Creates a new result for the specified request.
+     * 
+     * @param request The local artifact request, must not be {@code null}.
+     */
+    public LocalArtifactResult( LocalArtifactRequest request )
+    {
+        if ( request == null )
+        {
+            throw new IllegalArgumentException( "local artifact request has not been specified" );
+        }
+        this.request = request;
+    }
+
+    /**
+     * Gets the request corresponding to this result.
+     * 
+     * @return The corresponding request, never {@code null}.
+     */
+    public LocalArtifactRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the file to the requested artifact. Note that this file must not be used unless {@link #isAvailable()}
+     * returns {@code true}. An artifact file can be found but considered unavailable if the artifact was cached from a
+     * remote repository that is not part of the list of remote repositories used for the query.
+     * 
+     * @return The file to the requested artifact or {@code null} if the artifact does not exist locally.
+     */
+    public File getFile()
+    {
+        return file;
+    }
+
+    /**
+     * Sets the file to requested artifact.
+     * 
+     * @param file The artifact file, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public LocalArtifactResult setFile( File file )
+    {
+        this.file = file;
+        return this;
+    }
+
+    /**
+     * Indicates whether the requested artifact is available for use. As a minimum, the file needs to be physically
+     * existent in the local repository to be available. Additionally, a local repository manager can consider the list
+     * of supplied remote repositories to determine whether the artifact is logically available and mark an artifact
+     * unavailable (despite its physical existence) if it is not known to be hosted by any of the provided repositories.
+     * 
+     * @return {@code true} if the artifact is available, {@code false} otherwise.
+     * @see LocalArtifactRequest#getRepositories()
+     */
+    public boolean isAvailable()
+    {
+        return available;
+    }
+
+    /**
+     * Sets whether the artifact is available.
+     * 
+     * @param available {@code true} if the artifact is available, {@code false} otherwise.
+     * @return This result for chaining, never {@code null}.
+     */
+    public LocalArtifactResult setAvailable( boolean available )
+    {
+        this.available = available;
+        return this;
+    }
+
+    /**
+     * Gets the (first) remote repository from which the artifact was cached (if any).
+     * 
+     * @return The remote repository from which the artifact was originally retrieved or {@code null} if unknown or if
+     *         the artifact has been locally installed.
+     * @see LocalArtifactRequest#getRepositories()
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the (first) remote repository from which the artifact was cached.
+     * 
+     * @param repository The remote repository from which the artifact was originally retrieved, may be {@code null} if
+     *            unknown or if the artifact has been locally installed.
+     * @return This result for chaining, never {@code null}.
+     */
+    public LocalArtifactResult setRepository( RemoteRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getFile() + " (" + ( isAvailable() ? "available" : "unavailable" ) + ")";
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalMetadataRegistration.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalMetadataRegistration.java
new file mode 100644 (file)
index 0000000..a01ba3e
--- /dev/null
@@ -0,0 +1,139 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * A request to register metadata within the local repository.
+ * 
+ * @see LocalRepositoryManager#add(RepositorySystemSession, LocalMetadataRegistration)
+ */
+public final class LocalMetadataRegistration
+{
+
+    private Metadata metadata;
+
+    private RemoteRepository repository;
+
+    private Collection<String> contexts = Collections.emptyList();
+
+    /**
+     * Creates an uninitialized registration.
+     */
+    public LocalMetadataRegistration()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a registration request for the specified metadata accompanying a locally installed artifact.
+     * 
+     * @param metadata The metadata to register, may be {@code null}.
+     */
+    public LocalMetadataRegistration( Metadata metadata )
+    {
+        setMetadata( metadata );
+    }
+
+    /**
+     * Creates a registration request for the specified metadata.
+     * 
+     * @param metadata The metadata to register, may be {@code null}.
+     * @param repository The remote repository from which the metadata was resolved or {@code null} if the metadata
+     *            accompanies a locally installed artifact.
+     * @param contexts The resolution contexts, may be {@code null}.
+     */
+    public LocalMetadataRegistration( Metadata metadata, RemoteRepository repository, Collection<String> contexts )
+    {
+        setMetadata( metadata );
+        setRepository( repository );
+        setContexts( contexts );
+    }
+
+    /**
+     * Gets the metadata to register.
+     * 
+     * @return The metadata or {@code null} if not set.
+     */
+    public Metadata getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Sets the metadata to register.
+     * 
+     * @param metadata The metadata, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public LocalMetadataRegistration setMetadata( Metadata metadata )
+    {
+        this.metadata = metadata;
+        return this;
+    }
+
+    /**
+     * Gets the remote repository from which the metadata was resolved.
+     * 
+     * @return The remote repository or {@code null} if the metadata was locally installed.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the remote repository from which the metadata was resolved.
+     * 
+     * @param repository The remote repository or {@code null} if the metadata accompanies a locally installed artifact.
+     * @return This request for chaining, never {@code null}.
+     */
+    public LocalMetadataRegistration setRepository( RemoteRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    /**
+     * Gets the resolution contexts in which the metadata is available.
+     * 
+     * @return The resolution contexts in which the metadata is available, never {@code null}.
+     */
+    public Collection<String> getContexts()
+    {
+        return contexts;
+    }
+
+    /**
+     * Sets the resolution contexts in which the metadata is available.
+     * 
+     * @param contexts The resolution contexts, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public LocalMetadataRegistration setContexts( Collection<String> contexts )
+    {
+        if ( contexts != null )
+        {
+            this.contexts = contexts;
+        }
+        else
+        {
+            this.contexts = Collections.emptyList();
+        }
+        return this;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalMetadataRequest.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalMetadataRequest.java
new file mode 100644 (file)
index 0000000..0ee4dc5
--- /dev/null
@@ -0,0 +1,124 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * A query to the local repository for the existence of metadata.
+ * 
+ * @see LocalRepositoryManager#find(RepositorySystemSession, LocalMetadataRequest)
+ */
+public final class LocalMetadataRequest
+{
+
+    private Metadata metadata;
+
+    private String context = "";
+
+    private RemoteRepository repository = null;
+
+    /**
+     * Creates an uninitialized query.
+     */
+    public LocalMetadataRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a query with the specified properties.
+     * 
+     * @param metadata The metadata to query for, may be {@code null}.
+     * @param repository The source remote repository for the metadata, may be {@code null} for local metadata.
+     * @param context The resolution context for the metadata, may be {@code null}.
+     */
+    public LocalMetadataRequest( Metadata metadata, RemoteRepository repository, String context )
+    {
+        setMetadata( metadata );
+        setRepository( repository );
+        setContext( context );
+    }
+
+    /**
+     * Gets the metadata to query for.
+     * 
+     * @return The metadata or {@code null} if not set.
+     */
+    public Metadata getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Sets the metadata to query for.
+     * 
+     * @param metadata The metadata, may be {@code null}.
+     * @return This query for chaining, never {@code null}.
+     */
+    public LocalMetadataRequest setMetadata( Metadata metadata )
+    {
+        this.metadata = metadata;
+        return this;
+    }
+
+    /**
+     * Gets the resolution context.
+     * 
+     * @return The resolution context, never {@code null}.
+     */
+    public String getContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the resolution context.
+     * 
+     * @param context The resolution context, may be {@code null}.
+     * @return This query for chaining, never {@code null}.
+     */
+    public LocalMetadataRequest setContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Gets the remote repository to use as source of the metadata.
+     * 
+     * @return The remote repositories, may be {@code null} for local metadata.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the remote repository to use as sources of the metadata.
+     * 
+     * @param repository The remote repository, may be {@code null}.
+     * @return This query for chaining, may be {@code null} for local metadata.
+     */
+    public LocalMetadataRequest setRepository( RemoteRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getMetadata() + " @ " + getRepository();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalMetadataResult.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalMetadataResult.java
new file mode 100644 (file)
index 0000000..6f3687a
--- /dev/null
@@ -0,0 +1,105 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+import java.io.File;
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * A result from the local repository about the existence of metadata.
+ * 
+ * @see LocalRepositoryManager#find(RepositorySystemSession, LocalMetadataRequest)
+ */
+public final class LocalMetadataResult
+{
+
+    private final LocalMetadataRequest request;
+
+    private File file;
+
+    private boolean stale;
+
+    /**
+     * Creates a new result for the specified request.
+     * 
+     * @param request The local metadata request, must not be {@code null}.
+     */
+    public LocalMetadataResult( LocalMetadataRequest request )
+    {
+        if ( request == null )
+        {
+            throw new IllegalArgumentException( "local metadata request has not been specified" );
+        }
+        this.request = request;
+    }
+
+    /**
+     * Gets the request corresponding to this result.
+     * 
+     * @return The corresponding request, never {@code null}.
+     */
+    public LocalMetadataRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the file to the requested metadata if the metadata is available in the local repository.
+     * 
+     * @return The file to the requested metadata or {@code null}.
+     */
+    public File getFile()
+    {
+        return file;
+    }
+
+    /**
+     * Sets the file to requested metadata.
+     * 
+     * @param file The metadata file, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public LocalMetadataResult setFile( File file )
+    {
+        this.file = file;
+        return this;
+    }
+
+    /**
+     * This value indicates whether the metadata is stale and should be updated.
+     * 
+     * @return {@code true} if the metadata is stale and should be updated, {@code false} otherwise.
+     */
+    public boolean isStale()
+    {
+        return stale;
+    }
+
+    /**
+     * Sets whether the metadata is stale.
+     * 
+     * @param stale {@code true} if the metadata is stale and should be updated, {@code false} otherwise.
+     * @return This result for chaining, never {@code null}.
+     */
+    public LocalMetadataResult setStale( boolean stale )
+    {
+        this.stale = stale;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return request.toString() + "(" + getFile() + ")";
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalRepository.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalRepository.java
new file mode 100644 (file)
index 0000000..91b09d8
--- /dev/null
@@ -0,0 +1,123 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+import java.io.File;
+
+/**
+ * A repository on the local file system used to cache contents of remote repositories and to store locally installed
+ * artifacts. Note that this class merely describes such a repository, actual access to the contained artifacts is
+ * handled by a {@link LocalRepositoryManager} which is usually determined from the {@link #getContentType() type} of
+ * the repository.
+ */
+public final class LocalRepository
+    implements ArtifactRepository
+{
+
+    private final File basedir;
+
+    private final String type;
+
+    /**
+     * Creates a new local repository with the specified base directory and unknown type.
+     * 
+     * @param basedir The base directory of the repository, may be {@code null}.
+     */
+    public LocalRepository( String basedir )
+    {
+        this( ( basedir != null ) ? new File( basedir ) : null, "" );
+    }
+
+    /**
+     * Creates a new local repository with the specified base directory and unknown type.
+     * 
+     * @param basedir The base directory of the repository, may be {@code null}.
+     */
+    public LocalRepository( File basedir )
+    {
+        this( basedir, "" );
+    }
+
+    /**
+     * Creates a new local repository with the specified properties.
+     * 
+     * @param basedir The base directory of the repository, may be {@code null}.
+     * @param type The type of the repository, may be {@code null}.
+     */
+    public LocalRepository( File basedir, String type )
+    {
+        this.basedir = basedir;
+        this.type = ( type != null ) ? type : "";
+    }
+
+    public String getContentType()
+    {
+        return type;
+    }
+
+    public String getId()
+    {
+        return "local";
+    }
+
+    /**
+     * Gets the base directory of the repository.
+     * 
+     * @return The base directory or {@code null} if none.
+     */
+    public File getBasedir()
+    {
+        return basedir;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getBasedir() + " (" + getContentType() + ")";
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        LocalRepository that = (LocalRepository) obj;
+
+        return eq( basedir, that.basedir ) && eq( type, that.type );
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + hash( basedir );
+        hash = hash * 31 + hash( type );
+        return hash;
+    }
+
+    private static int hash( Object obj )
+    {
+        return obj != null ? obj.hashCode() : 0;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalRepositoryManager.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/LocalRepositoryManager.java
new file mode 100644 (file)
index 0000000..d9d8777
--- /dev/null
@@ -0,0 +1,118 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * Manages access to a local repository.
+ * 
+ * @see RepositorySystemSession#getLocalRepositoryManager()
+ * @see org.eclipse.aether.RepositorySystem#newLocalRepositoryManager(RepositorySystemSession, LocalRepository)
+ */
+public interface LocalRepositoryManager
+{
+
+    /**
+     * Gets the description of the local repository being managed.
+     * 
+     * @return The description of the local repository, never {@code null}.
+     */
+    LocalRepository getRepository();
+
+    /**
+     * Gets the relative path for a locally installed artifact. Note that the artifact need not actually exist yet at
+     * the returned location, the path merely indicates where the artifact would eventually be stored. The path uses the
+     * forward slash as directory separator regardless of the underlying file system.
+     * 
+     * @param artifact The artifact for which to determine the path, must not be {@code null}.
+     * @return The path, relative to the local repository's base directory.
+     */
+    String getPathForLocalArtifact( Artifact artifact );
+
+    /**
+     * Gets the relative path for an artifact cached from a remote repository. Note that the artifact need not actually
+     * exist yet at the returned location, the path merely indicates where the artifact would eventually be stored. The
+     * path uses the forward slash as directory separator regardless of the underlying file system.
+     * 
+     * @param artifact The artifact for which to determine the path, must not be {@code null}.
+     * @param repository The source repository of the artifact, must not be {@code null}.
+     * @param context The resolution context in which the artifact is being requested, may be {@code null}.
+     * @return The path, relative to the local repository's base directory.
+     */
+    String getPathForRemoteArtifact( Artifact artifact, RemoteRepository repository, String context );
+
+    /**
+     * Gets the relative path for locally installed metadata. Note that the metadata need not actually exist yet at the
+     * returned location, the path merely indicates where the metadata would eventually be stored. The path uses the
+     * forward slash as directory separator regardless of the underlying file system.
+     * 
+     * @param metadata The metadata for which to determine the path, must not be {@code null}.
+     * @return The path, relative to the local repository's base directory.
+     */
+    String getPathForLocalMetadata( Metadata metadata );
+
+    /**
+     * Gets the relative path for metadata cached from a remote repository. Note that the metadata need not actually
+     * exist yet at the returned location, the path merely indicates where the metadata would eventually be stored. The
+     * path uses the forward slash as directory separator regardless of the underlying file system.
+     * 
+     * @param metadata The metadata for which to determine the path, must not be {@code null}.
+     * @param repository The source repository of the metadata, must not be {@code null}.
+     * @param context The resolution context in which the metadata is being requested, may be {@code null}.
+     * @return The path, relative to the local repository's base directory.
+     */
+    String getPathForRemoteMetadata( Metadata metadata, RemoteRepository repository, String context );
+
+    /**
+     * Queries for the existence of an artifact in the local repository. The request could be satisfied by a locally
+     * installed artifact or a previously downloaded artifact.
+     * 
+     * @param session The repository system session during which the request is made, must not be {@code null}.
+     * @param request The artifact request, must not be {@code null}.
+     * @return The result of the request, never {@code null}.
+     */
+    LocalArtifactResult find( RepositorySystemSession session, LocalArtifactRequest request );
+
+    /**
+     * Registers an installed or resolved artifact with the local repository. Note that artifact registration is merely
+     * concerned about updating the local repository's internal state, not about actually installing the artifact or its
+     * accompanying metadata.
+     * 
+     * @param session The repository system session during which the registration is made, must not be {@code null}.
+     * @param request The registration request, must not be {@code null}.
+     */
+    void add( RepositorySystemSession session, LocalArtifactRegistration request );
+
+    /**
+     * Queries for the existence of metadata in the local repository. The request could be satisfied by locally
+     * installed or previously downloaded metadata.
+     * 
+     * @param session The repository system session during which the request is made, must not be {@code null}.
+     * @param request The metadata request, must not be {@code null}.
+     * @return The result of the request, never {@code null}.
+     */
+    LocalMetadataResult find( RepositorySystemSession session, LocalMetadataRequest request );
+
+    /**
+     * Registers installed or resolved metadata with the local repository. Note that metadata registration is merely
+     * concerned about updating the local repository's internal state, not about actually installing the metadata.
+     * However, this method MUST be called after the actual install to give the repository manager the opportunity to
+     * inspect the added metadata.
+     * 
+     * @param session The repository system session during which the registration is made, must not be {@code null}.
+     * @param request The registration request, must not be {@code null}.
+     */
+    void add( RepositorySystemSession session, LocalMetadataRegistration request );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/MirrorSelector.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/MirrorSelector.java
new file mode 100644 (file)
index 0000000..1614acc
--- /dev/null
@@ -0,0 +1,30 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+/**
+ * Selects a mirror for a given remote repository.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getMirrorSelector()
+ */
+public interface MirrorSelector
+{
+
+    /**
+     * Selects a mirror for the specified repository.
+     * 
+     * @param repository The repository to select a mirror for, must not be {@code null}.
+     * @return The selected mirror or {@code null} if none.
+     * @see RemoteRepository#getMirroredRepositories()
+     */
+    RemoteRepository getMirror( RemoteRepository repository );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/NoLocalRepositoryManagerException.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/NoLocalRepositoryManagerException.java
new file mode 100644 (file)
index 0000000..203ccfb
--- /dev/null
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of an unsupported local repository type.
+ */
+public class NoLocalRepositoryManagerException
+    extends RepositoryException
+{
+
+    private final transient LocalRepository repository;
+
+    /**
+     * Creates a new exception with the specified repository.
+     * 
+     * @param repository The local repository for which no support is available, may be {@code null}.
+     */
+    public NoLocalRepositoryManagerException( LocalRepository repository )
+    {
+        this( repository, toMessage( repository ) );
+    }
+
+    /**
+     * Creates a new exception with the specified repository and detail message.
+     * 
+     * @param repository The local repository for which no support is available, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public NoLocalRepositoryManagerException( LocalRepository repository, String message )
+    {
+        super( message );
+        this.repository = repository;
+    }
+
+    /**
+     * Creates a new exception with the specified repository and cause.
+     * 
+     * @param repository The local repository for which no support is available, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public NoLocalRepositoryManagerException( LocalRepository repository, Throwable cause )
+    {
+        this( repository, toMessage( repository ), cause );
+    }
+
+    /**
+     * Creates a new exception with the specified repository, detail message and cause.
+     * 
+     * @param repository The local repository for which no support is available, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public NoLocalRepositoryManagerException( LocalRepository repository, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.repository = repository;
+    }
+
+    private static String toMessage( LocalRepository repository )
+    {
+        if ( repository != null )
+        {
+            return "No manager available for local repository (" + repository.getBasedir().getAbsolutePath()
+                + ") of type " + repository.getContentType();
+        }
+        else
+        {
+            return "No manager available for local repository";
+        }
+    }
+
+    /**
+     * Gets the local repository whose content type is not supported.
+     * 
+     * @return The unsupported local repository or {@code null} if unknown.
+     */
+    public LocalRepository getRepository()
+    {
+        return repository;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/Proxy.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/Proxy.java
new file mode 100644 (file)
index 0000000..b575bbc
--- /dev/null
@@ -0,0 +1,149 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+/**
+ * A proxy to use for connections to a repository.
+ */
+public final class Proxy
+{
+
+    /**
+     * Type denoting a proxy for HTTP transfers.
+     */
+    public static final String TYPE_HTTP = "http";
+
+    /**
+     * Type denoting a proxy for HTTPS transfers.
+     */
+    public static final String TYPE_HTTPS = "https";
+
+    private final String type;
+
+    private final String host;
+
+    private final int port;
+
+    private final Authentication auth;
+
+    /**
+     * Creates a new proxy with the specified properties and no authentication.
+     * 
+     * @param type The type of the proxy, e.g. "http", may be {@code null}.
+     * @param host The host of the proxy, may be {@code null}.
+     * @param port The port of the proxy.
+     */
+    public Proxy( String type, String host, int port )
+    {
+        this( type, host, port, null );
+    }
+
+    /**
+     * Creates a new proxy with the specified properties.
+     * 
+     * @param type The type of the proxy, e.g. "http", may be {@code null}.
+     * @param host The host of the proxy, may be {@code null}.
+     * @param port The port of the proxy.
+     * @param auth The authentication to use for the proxy connection, may be {@code null}.
+     */
+    public Proxy( String type, String host, int port, Authentication auth )
+    {
+        this.type = ( type != null ) ? type : "";
+        this.host = ( host != null ) ? host : "";
+        this.port = port;
+        this.auth = auth;
+    }
+
+    /**
+     * Gets the type of this proxy.
+     * 
+     * @return The type of this proxy, never {@code null}.
+     */
+    public String getType()
+    {
+        return type;
+    }
+
+    /**
+     * Gets the host for this proxy.
+     * 
+     * @return The host for this proxy, never {@code null}.
+     */
+    public String getHost()
+    {
+        return host;
+    }
+
+    /**
+     * Gets the port number for this proxy.
+     * 
+     * @return The port number for this proxy.
+     */
+    public int getPort()
+    {
+        return port;
+    }
+
+    /**
+     * Gets the authentication to use for the proxy connection.
+     * 
+     * @return The authentication to use or {@code null} if none.
+     */
+    public Authentication getAuthentication()
+    {
+        return auth;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getHost() + ':' + getPort();
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        Proxy that = (Proxy) obj;
+
+        return eq( type, that.type ) && eq( host, that.host ) && port == that.port && eq( auth, that.auth );
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + hash( host );
+        hash = hash * 31 + hash( type );
+        hash = hash * 31 + port;
+        hash = hash * 31 + hash( auth );
+        return hash;
+    }
+
+    private static int hash( Object obj )
+    {
+        return obj != null ? obj.hashCode() : 0;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/ProxySelector.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/ProxySelector.java
new file mode 100644 (file)
index 0000000..680474c
--- /dev/null
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+/**
+ * Selects a proxy for a given remote repository.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getProxySelector()
+ */
+public interface ProxySelector
+{
+
+    /**
+     * Selects a proxy for the specified remote repository.
+     * 
+     * @param repository The repository for which to select a proxy, must not be {@code null}.
+     * @return The selected proxy or {@code null} if none.
+     */
+    Proxy getProxy( RemoteRepository repository );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/RemoteRepository.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/RemoteRepository.java
new file mode 100644 (file)
index 0000000..aaa9acc
--- /dev/null
@@ -0,0 +1,573 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A repository on a remote server.
+ */
+public final class RemoteRepository
+    implements ArtifactRepository
+{
+
+    private static final Pattern URL_PATTERN =
+        Pattern.compile( "([^:/]+(:[^:/]{2,}+(?=://))?):(//([^@/]*@)?([^/:]+))?.*" );
+
+    private final String id;
+
+    private final String type;
+
+    private final String url;
+
+    private final String host;
+
+    private final String protocol;
+
+    private final RepositoryPolicy releasePolicy;
+
+    private final RepositoryPolicy snapshotPolicy;
+
+    private final Proxy proxy;
+
+    private final Authentication authentication;
+
+    private final List<RemoteRepository> mirroredRepositories;
+
+    private final boolean repositoryManager;
+
+    RemoteRepository( Builder builder )
+    {
+        if ( builder.prototype != null )
+        {
+            id = ( builder.delta & Builder.ID ) != 0 ? builder.id : builder.prototype.id;
+            type = ( builder.delta & Builder.TYPE ) != 0 ? builder.type : builder.prototype.type;
+            url = ( builder.delta & Builder.URL ) != 0 ? builder.url : builder.prototype.url;
+            releasePolicy =
+                ( builder.delta & Builder.RELEASES ) != 0 ? builder.releasePolicy : builder.prototype.releasePolicy;
+            snapshotPolicy =
+                ( builder.delta & Builder.SNAPSHOTS ) != 0 ? builder.snapshotPolicy : builder.prototype.snapshotPolicy;
+            proxy = ( builder.delta & Builder.PROXY ) != 0 ? builder.proxy : builder.prototype.proxy;
+            authentication =
+                ( builder.delta & Builder.AUTH ) != 0 ? builder.authentication : builder.prototype.authentication;
+            repositoryManager =
+                ( builder.delta & Builder.REPOMAN ) != 0 ? builder.repositoryManager
+                                : builder.prototype.repositoryManager;
+            mirroredRepositories =
+                ( builder.delta & Builder.MIRRORED ) != 0 ? copy( builder.mirroredRepositories )
+                                : builder.prototype.mirroredRepositories;
+        }
+        else
+        {
+            id = builder.id;
+            type = builder.type;
+            url = builder.url;
+            releasePolicy = builder.releasePolicy;
+            snapshotPolicy = builder.snapshotPolicy;
+            proxy = builder.proxy;
+            authentication = builder.authentication;
+            repositoryManager = builder.repositoryManager;
+            mirroredRepositories = copy( builder.mirroredRepositories );
+        }
+
+        Matcher m = URL_PATTERN.matcher( url );
+        if ( m.matches() )
+        {
+            protocol = m.group( 1 );
+            String host = m.group( 5 );
+            this.host = ( host != null ) ? host : "";
+        }
+        else
+        {
+            protocol = host = "";
+        }
+    }
+
+    private static List<RemoteRepository> copy( List<RemoteRepository> repos )
+    {
+        if ( repos == null || repos.isEmpty() )
+        {
+            return Collections.emptyList();
+        }
+        return Collections.unmodifiableList( Arrays.asList( repos.toArray( new RemoteRepository[repos.size()] ) ) );
+    }
+
+    public String getId()
+    {
+        return id;
+    }
+
+    public String getContentType()
+    {
+        return type;
+    }
+
+    /**
+     * Gets the (base) URL of this repository.
+     * 
+     * @return The (base) URL of this repository, never {@code null}.
+     */
+    public String getUrl()
+    {
+        return url;
+    }
+
+    /**
+     * Gets the protocol part from the repository's URL, for example {@code file} or {@code http}. As suggested by RFC
+     * 2396, section 3.1 "Scheme Component", the protocol name should be treated case-insensitively.
+     * 
+     * @return The protocol or an empty string if none, never {@code null}.
+     */
+    public String getProtocol()
+    {
+        return protocol;
+    }
+
+    /**
+     * Gets the host part from the repository's URL.
+     * 
+     * @return The host or an empty string if none, never {@code null}.
+     */
+    public String getHost()
+    {
+        return host;
+    }
+
+    /**
+     * Gets the policy to apply for snapshot/release artifacts.
+     * 
+     * @param snapshot {@code true} to retrieve the snapshot policy, {@code false} to retrieve the release policy.
+     * @return The requested repository policy, never {@code null}.
+     */
+    public RepositoryPolicy getPolicy( boolean snapshot )
+    {
+        return snapshot ? snapshotPolicy : releasePolicy;
+    }
+
+    /**
+     * Gets the proxy that has been selected for this repository.
+     * 
+     * @return The selected proxy or {@code null} if none.
+     */
+    public Proxy getProxy()
+    {
+        return proxy;
+    }
+
+    /**
+     * Gets the authentication that has been selected for this repository.
+     * 
+     * @return The selected authentication or {@code null} if none.
+     */
+    public Authentication getAuthentication()
+    {
+        return authentication;
+    }
+
+    /**
+     * Gets the repositories that this repository serves as a mirror for.
+     * 
+     * @return The (read-only) repositories being mirrored by this repository, never {@code null}.
+     */
+    public List<RemoteRepository> getMirroredRepositories()
+    {
+        return mirroredRepositories;
+    }
+
+    /**
+     * Indicates whether this repository refers to a repository manager or not.
+     * 
+     * @return {@code true} if this repository is a repository manager, {@code false} otherwise.
+     */
+    public boolean isRepositoryManager()
+    {
+        return repositoryManager;
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+        buffer.append( getId() );
+        buffer.append( " (" ).append( getUrl() );
+        buffer.append( ", " ).append( getContentType() );
+        boolean r = getPolicy( false ).isEnabled(), s = getPolicy( true ).isEnabled();
+        if ( r && s )
+        {
+            buffer.append( ", releases+snapshots" );
+        }
+        else if ( r )
+        {
+            buffer.append( ", releases" );
+        }
+        else if ( s )
+        {
+            buffer.append( ", snapshots" );
+        }
+        else
+        {
+            buffer.append( ", disabled" );
+        }
+        if ( isRepositoryManager() )
+        {
+            buffer.append( ", managed" );
+        }
+        buffer.append( ")" );
+        return buffer.toString();
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        RemoteRepository that = (RemoteRepository) obj;
+
+        return eq( url, that.url ) && eq( type, that.type ) && eq( id, that.id )
+            && eq( releasePolicy, that.releasePolicy ) && eq( snapshotPolicy, that.snapshotPolicy )
+            && eq( proxy, that.proxy ) && eq( authentication, that.authentication )
+            && eq( mirroredRepositories, that.mirroredRepositories ) && repositoryManager == that.repositoryManager;
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + hash( url );
+        hash = hash * 31 + hash( type );
+        hash = hash * 31 + hash( id );
+        hash = hash * 31 + hash( releasePolicy );
+        hash = hash * 31 + hash( snapshotPolicy );
+        hash = hash * 31 + hash( proxy );
+        hash = hash * 31 + hash( authentication );
+        hash = hash * 31 + hash( mirroredRepositories );
+        hash = hash * 31 + ( repositoryManager ? 1 : 0 );
+        return hash;
+    }
+
+    private static int hash( Object obj )
+    {
+        return obj != null ? obj.hashCode() : 0;
+    }
+
+    /**
+     * A builder to create remote repositories.
+     */
+    public static final class Builder
+    {
+
+        private static final RepositoryPolicy DEFAULT_POLICY = new RepositoryPolicy();
+
+        static final int ID = 0x0001, TYPE = 0x0002, URL = 0x0004, RELEASES = 0x0008, SNAPSHOTS = 0x0010,
+                        PROXY = 0x0020, AUTH = 0x0040, MIRRORED = 0x0080, REPOMAN = 0x0100;
+
+        int delta;
+
+        RemoteRepository prototype;
+
+        String id;
+
+        String type;
+
+        String url;
+
+        RepositoryPolicy releasePolicy = DEFAULT_POLICY;
+
+        RepositoryPolicy snapshotPolicy = DEFAULT_POLICY;
+
+        Proxy proxy;
+
+        Authentication authentication;
+
+        List<RemoteRepository> mirroredRepositories;
+
+        boolean repositoryManager;
+
+        /**
+         * Creates a new repository builder.
+         * 
+         * @param id The identifier of the repository, may be {@code null}.
+         * @param type The type of the repository, may be {@code null}.
+         * @param url The (base) URL of the repository, may be {@code null}.
+         */
+        public Builder( String id, String type, String url )
+        {
+            this.id = ( id != null ) ? id : "";
+            this.type = ( type != null ) ? type : "";
+            this.url = ( url != null ) ? url : "";
+        }
+
+        /**
+         * Creates a new repository builder which uses the specified remote repository as a prototype for the new one.
+         * All properties which have not been set on the builder will be copied from the prototype when building the
+         * repository.
+         * 
+         * @param prototype The remote repository to use as prototype, must not be {@code null}.
+         */
+        public Builder( RemoteRepository prototype )
+        {
+            if ( prototype == null )
+            {
+                throw new IllegalArgumentException( "repository prototype missing" );
+            }
+            this.prototype = prototype;
+        }
+
+        /**
+         * Builds a new remote repository from the current values of this builder. The state of the builder itself
+         * remains unchanged.
+         * 
+         * @return The remote repository, never {@code null}.
+         */
+        public RemoteRepository build()
+        {
+            if ( prototype != null && delta == 0 )
+            {
+                return prototype;
+            }
+            return new RemoteRepository( this );
+        }
+
+        private <T> void delta( int flag, T builder, T prototype )
+        {
+            boolean equal = ( builder != null ) ? builder.equals( prototype ) : prototype == null;
+            if ( equal )
+            {
+                delta &= ~flag;
+            }
+            else
+            {
+                delta |= flag;
+            }
+        }
+
+        /**
+         * Sets the identifier of the repository.
+         * 
+         * @param id The identifier of the repository, may be {@code null}.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setId( String id )
+        {
+            this.id = ( id != null ) ? id : "";
+            if ( prototype != null )
+            {
+                delta( ID, this.id, prototype.getId() );
+            }
+            return this;
+        }
+
+        /**
+         * Sets the type of the repository, e.g. "default".
+         * 
+         * @param type The type of the repository, may be {@code null}.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setContentType( String type )
+        {
+            this.type = ( type != null ) ? type : "";
+            if ( prototype != null )
+            {
+                delta( TYPE, this.type, prototype.getContentType() );
+            }
+            return this;
+        }
+
+        /**
+         * Sets the (base) URL of the repository.
+         * 
+         * @param url The URL of the repository, may be {@code null}.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setUrl( String url )
+        {
+            this.url = ( url != null ) ? url : "";
+            if ( prototype != null )
+            {
+                delta( URL, this.url, prototype.getUrl() );
+            }
+            return this;
+        }
+
+        /**
+         * Sets the policy to apply for snapshot and release artifacts.
+         * 
+         * @param policy The repository policy to set, may be {@code null} to use a default policy.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setPolicy( RepositoryPolicy policy )
+        {
+            this.releasePolicy = this.snapshotPolicy = ( policy != null ) ? policy : DEFAULT_POLICY;
+            if ( prototype != null )
+            {
+                delta( RELEASES, this.releasePolicy, prototype.getPolicy( false ) );
+                delta( SNAPSHOTS, this.snapshotPolicy, prototype.getPolicy( true ) );
+            }
+            return this;
+        }
+
+        /**
+         * Sets the policy to apply for release artifacts.
+         * 
+         * @param releasePolicy The repository policy to set, may be {@code null} to use a default policy.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setReleasePolicy( RepositoryPolicy releasePolicy )
+        {
+            this.releasePolicy = ( releasePolicy != null ) ? releasePolicy : DEFAULT_POLICY;
+            if ( prototype != null )
+            {
+                delta( RELEASES, this.releasePolicy, prototype.getPolicy( false ) );
+            }
+            return this;
+        }
+
+        /**
+         * Sets the policy to apply for snapshot artifacts.
+         * 
+         * @param snapshotPolicy The repository policy to set, may be {@code null} to use a default policy.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setSnapshotPolicy( RepositoryPolicy snapshotPolicy )
+        {
+            this.snapshotPolicy = ( snapshotPolicy != null ) ? snapshotPolicy : DEFAULT_POLICY;
+            if ( prototype != null )
+            {
+                delta( SNAPSHOTS, this.snapshotPolicy, prototype.getPolicy( true ) );
+            }
+            return this;
+        }
+
+        /**
+         * Sets the proxy to use in order to access the repository.
+         * 
+         * @param proxy The proxy to use, may be {@code null}.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setProxy( Proxy proxy )
+        {
+            this.proxy = proxy;
+            if ( prototype != null )
+            {
+                delta( PROXY, this.proxy, prototype.getProxy() );
+            }
+            return this;
+        }
+
+        /**
+         * Sets the authentication to use in order to access the repository.
+         * 
+         * @param authentication The authentication to use, may be {@code null}.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setAuthentication( Authentication authentication )
+        {
+            this.authentication = authentication;
+            if ( prototype != null )
+            {
+                delta( AUTH, this.authentication, prototype.getAuthentication() );
+            }
+            return this;
+        }
+
+        /**
+         * Sets the repositories being mirrored by the repository.
+         * 
+         * @param mirroredRepositories The repositories being mirrored by the repository, may be {@code null}.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setMirroredRepositories( List<RemoteRepository> mirroredRepositories )
+        {
+            if ( this.mirroredRepositories == null )
+            {
+                this.mirroredRepositories = new ArrayList<RemoteRepository>();
+            }
+            else
+            {
+                this.mirroredRepositories.clear();
+            }
+            if ( mirroredRepositories != null )
+            {
+                this.mirroredRepositories.addAll( mirroredRepositories );
+            }
+            if ( prototype != null )
+            {
+                delta( MIRRORED, this.mirroredRepositories, prototype.getMirroredRepositories() );
+            }
+            return this;
+        }
+
+        /**
+         * Adds the specified repository to the list of repositories being mirrored by the repository. If this builder
+         * was {@link #RemoteRepository.Builder(RemoteRepository) constructed from a prototype}, the given repository
+         * will be added to the list of mirrored repositories from the prototype.
+         * 
+         * @param mirroredRepository The repository being mirrored by the repository, may be {@code null}.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder addMirroredRepository( RemoteRepository mirroredRepository )
+        {
+            if ( mirroredRepository != null )
+            {
+                if ( this.mirroredRepositories == null )
+                {
+                    this.mirroredRepositories = new ArrayList<RemoteRepository>();
+                    if ( prototype != null )
+                    {
+                        mirroredRepositories.addAll( prototype.getMirroredRepositories() );
+                    }
+                }
+                mirroredRepositories.add( mirroredRepository );
+                if ( prototype != null )
+                {
+                    delta |= MIRRORED;
+                }
+            }
+            return this;
+        }
+
+        /**
+         * Marks the repository as a repository manager or not.
+         * 
+         * @param repositoryManager {@code true} if the repository points at a repository manager, {@code false} if the
+         *            repository is just serving static contents.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setRepositoryManager( boolean repositoryManager )
+        {
+            this.repositoryManager = repositoryManager;
+            if ( prototype != null )
+            {
+                delta( REPOMAN, this.repositoryManager, prototype.isRepositoryManager() );
+            }
+            return this;
+        }
+
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/RepositoryPolicy.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/RepositoryPolicy.java
new file mode 100644 (file)
index 0000000..05224a8
--- /dev/null
@@ -0,0 +1,152 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+/**
+ * A policy controlling access to a repository.
+ */
+public final class RepositoryPolicy
+{
+
+    /**
+     * Never update locally cached data.
+     */
+    public static final String UPDATE_POLICY_NEVER = "never";
+
+    /**
+     * Always update locally cached data.
+     */
+    public static final String UPDATE_POLICY_ALWAYS = "always";
+
+    /**
+     * Update locally cached data once a day.
+     */
+    public static final String UPDATE_POLICY_DAILY = "daily";
+
+    /**
+     * Update locally cached data every X minutes as given by "interval:X".
+     */
+    public static final String UPDATE_POLICY_INTERVAL = "interval";
+
+    /**
+     * Verify checksums and fail the resolution if they do not match.
+     */
+    public static final String CHECKSUM_POLICY_FAIL = "fail";
+
+    /**
+     * Verify checksums and warn if they do not match.
+     */
+    public static final String CHECKSUM_POLICY_WARN = "warn";
+
+    /**
+     * Do not verify checksums.
+     */
+    public static final String CHECKSUM_POLICY_IGNORE = "ignore";
+
+    private final boolean enabled;
+
+    private final String updatePolicy;
+
+    private final String checksumPolicy;
+
+    /**
+     * Creates a new policy with checksum warnings and daily update checks.
+     */
+    public RepositoryPolicy()
+    {
+        this( true, UPDATE_POLICY_DAILY, CHECKSUM_POLICY_WARN );
+    }
+
+    /**
+     * Creates a new policy with the specified settings.
+     * 
+     * @param enabled A flag whether the associated repository should be accessed or not.
+     * @param updatePolicy The update interval after which locally cached data from the repository is considered stale
+     *            and should be refetched, may be {@code null}.
+     * @param checksumPolicy The way checksum verification should be handled, may be {@code null}.
+     */
+    public RepositoryPolicy( boolean enabled, String updatePolicy, String checksumPolicy )
+    {
+        this.enabled = enabled;
+        this.updatePolicy = ( updatePolicy != null ) ? updatePolicy : "";
+        this.checksumPolicy = ( checksumPolicy != null ) ? checksumPolicy : "";
+    }
+
+    /**
+     * Indicates whether the associated repository should be contacted or not.
+     * 
+     * @return {@code true} if the repository should be contacted, {@code false} otherwise.
+     */
+    public boolean isEnabled()
+    {
+        return enabled;
+    }
+
+    /**
+     * Gets the update policy for locally cached data from the repository.
+     * 
+     * @return The update policy, never {@code null}.
+     */
+    public String getUpdatePolicy()
+    {
+        return updatePolicy;
+    }
+
+    /**
+     * Gets the policy for checksum validation.
+     * 
+     * @return The checksum policy, never {@code null}.
+     */
+    public String getChecksumPolicy()
+    {
+        return checksumPolicy;
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+        buffer.append( "enabled=" ).append( isEnabled() );
+        buffer.append( ", checksums=" ).append( getChecksumPolicy() );
+        buffer.append( ", updates=" ).append( getUpdatePolicy() );
+        return buffer.toString();
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        RepositoryPolicy that = (RepositoryPolicy) obj;
+
+        return enabled == that.enabled && updatePolicy.equals( that.updatePolicy )
+            && checksumPolicy.equals( that.checksumPolicy );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + ( enabled ? 1 : 0 );
+        hash = hash * 31 + updatePolicy.hashCode();
+        hash = hash * 31 + checksumPolicy.hashCode();
+        return hash;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/WorkspaceReader.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/WorkspaceReader.java
new file mode 100644 (file)
index 0000000..570f6b6
--- /dev/null
@@ -0,0 +1,49 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+import java.io.File;
+import java.util.List;
+
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * Manages a repository backed by the IDE workspace, a build session or a similar ad-hoc collection of artifacts.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getWorkspaceReader()
+ */
+public interface WorkspaceReader
+{
+
+    /**
+     * Gets a description of the workspace repository.
+     * 
+     * @return The repository description, never {@code null}.
+     */
+    WorkspaceRepository getRepository();
+
+    /**
+     * Locates the specified artifact.
+     * 
+     * @param artifact The artifact to locate, must not be {@code null}.
+     * @return The path to the artifact or {@code null} if the artifact is not available.
+     */
+    File findArtifact( Artifact artifact );
+
+    /**
+     * Determines all available versions of the specified artifact.
+     * 
+     * @param artifact The artifact whose versions should be listed, must not be {@code null}.
+     * @return The available versions of the artifact, must not be {@code null}.
+     */
+    List<String> findVersions( Artifact artifact );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/WorkspaceRepository.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/WorkspaceRepository.java
new file mode 100644 (file)
index 0000000..811c589
--- /dev/null
@@ -0,0 +1,113 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.repository;
+
+import java.util.UUID;
+
+/**
+ * A repository backed by an IDE workspace, the output of a build session or similar ad-hoc collection of artifacts. As
+ * far as the repository system is concerned, a workspace repository is read-only, i.e. can only be used for artifact
+ * resolution but not installation/deployment. Note that this class merely describes such a repository, actual access to
+ * the contained artifacts is handled by a {@link WorkspaceReader}.
+ */
+public final class WorkspaceRepository
+    implements ArtifactRepository
+{
+
+    private final String type;
+
+    private final Object key;
+
+    /**
+     * Creates a new workspace repository of type {@code "workspace"} and a random key.
+     */
+    public WorkspaceRepository()
+    {
+        this( "workspace" );
+    }
+
+    /**
+     * Creates a new workspace repository with the specified type and a random key.
+     * 
+     * @param type The type of the repository, may be {@code null}.
+     */
+    public WorkspaceRepository( String type )
+    {
+        this( type, null );
+    }
+
+    /**
+     * Creates a new workspace repository with the specified type and key. The key is used to distinguish one workspace
+     * from another and should be sensitive to the artifacts that are (potentially) available in the workspace.
+     * 
+     * @param type The type of the repository, may be {@code null}.
+     * @param key The (comparison) key for the repository, may be {@code null} to generate a unique random key.
+     */
+    public WorkspaceRepository( String type, Object key )
+    {
+        this.type = ( type != null ) ? type : "";
+        this.key = ( key != null ) ? key : UUID.randomUUID().toString().replace( "-", "" );
+    }
+
+    public String getContentType()
+    {
+        return type;
+    }
+
+    public String getId()
+    {
+        return "workspace";
+    }
+
+    /**
+     * Gets the key of this workspace repository. The key is used to distinguish one workspace from another and should
+     * be sensitive to the artifacts that are (potentially) available in the workspace.
+     * 
+     * @return The (comparison) key for this workspace repository, never {@code null}.
+     */
+    public Object getKey()
+    {
+        return key;
+    }
+
+    @Override
+    public String toString()
+    {
+        return "(" + getContentType() + ")";
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        WorkspaceRepository that = (WorkspaceRepository) obj;
+
+        return getContentType().equals( that.getContentType() ) && getKey().equals( that.getKey() );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + getKey().hashCode();
+        hash = hash * 31 + getContentType().hashCode();
+        return hash;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/repository/package-info.java b/org.argeo.slc.repo/src/org/eclipse/aether/repository/package-info.java
new file mode 100644 (file)
index 0000000..21ab2bc
--- /dev/null
@@ -0,0 +1,15 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+/**
+ * The definition of various kinds of repositories that host artifacts.
+ */
+package org.eclipse.aether.repository;
+
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactDescriptorException.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactDescriptorException.java
new file mode 100644 (file)
index 0000000..7dae7f4
--- /dev/null
@@ -0,0 +1,82 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of an unreadable or unresolvable artifact descriptor.
+ */
+public class ArtifactDescriptorException
+    extends RepositoryException
+{
+
+    private final transient ArtifactDescriptorResult result;
+
+    /**
+     * Creates a new exception with the specified result.
+     * 
+     * @param result The descriptor result at the point the exception occurred, may be {@code null}.
+     */
+    public ArtifactDescriptorException( ArtifactDescriptorResult result )
+    {
+        super( "Failed to read artifact descriptor"
+            + ( result != null ? " for " + result.getRequest().getArtifact() : "" ), getCause( result ) );
+        this.result = result;
+    }
+
+    /**
+     * Creates a new exception with the specified result and detail message.
+     * 
+     * @param result The descriptor result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public ArtifactDescriptorException( ArtifactDescriptorResult result, String message )
+    {
+        super( message, getCause( result ) );
+        this.result = result;
+    }
+
+    /**
+     * Creates a new exception with the specified result, detail message and cause.
+     * 
+     * @param result The descriptor result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public ArtifactDescriptorException( ArtifactDescriptorResult result, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.result = result;
+    }
+
+    /**
+     * Gets the descriptor result at the point the exception occurred. Despite being incomplete, callers might want to
+     * use this result to fail gracefully and continue their operation with whatever interim data has been gathered.
+     * 
+     * @return The descriptor result or {@code null} if unknown.
+     */
+    public ArtifactDescriptorResult getResult()
+    {
+        return result;
+    }
+
+    private static Throwable getCause( ArtifactDescriptorResult result )
+    {
+        Throwable cause = null;
+        if ( result != null && !result.getExceptions().isEmpty() )
+        {
+            cause = result.getExceptions().get( 0 );
+        }
+        return cause;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactDescriptorPolicy.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactDescriptorPolicy.java
new file mode 100644 (file)
index 0000000..ec519fe
--- /dev/null
@@ -0,0 +1,52 @@
+/*******************************************************************************
+ * Copyright (c) 2012, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * Controls the handling of errors related to reading an artifact descriptor.
+ * 
+ * @see RepositorySystemSession#getArtifactDescriptorPolicy()
+ */
+public interface ArtifactDescriptorPolicy
+{
+
+    /**
+     * Bit mask indicating that errors while reading the artifact descriptor should not be tolerated.
+     */
+    int STRICT = 0x00;
+
+    /**
+     * Bit flag indicating that missing artifact descriptors should be silently ignored.
+     */
+    int IGNORE_MISSING = 0x01;
+
+    /**
+     * Bit flag indicating that existent but invalid artifact descriptors should be silently ignored.
+     */
+    int IGNORE_INVALID = 0x02;
+
+    /**
+     * Bit mask indicating that all errors should be silently ignored.
+     */
+    int IGNORE_ERRORS = IGNORE_MISSING | IGNORE_INVALID;
+
+    /**
+     * Gets the error policy for an artifact's descriptor.
+     * 
+     * @param session The repository session during which the policy is determined, must not be {@code null}.
+     * @param request The policy request holding further details, must not be {@code null}.
+     * @return The bit mask describing the desired error policy.
+     */
+    int getPolicy( RepositorySystemSession session, ArtifactDescriptorPolicyRequest request );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactDescriptorPolicyRequest.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactDescriptorPolicyRequest.java
new file mode 100644 (file)
index 0000000..2edf0c5
--- /dev/null
@@ -0,0 +1,97 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * A query for the error policy for a given artifact's descriptor.
+ * 
+ * @see ArtifactDescriptorPolicy
+ */
+public final class ArtifactDescriptorPolicyRequest
+{
+
+    private Artifact artifact;
+
+    private String context = "";
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public ArtifactDescriptorPolicyRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request for the specified artifact.
+     * 
+     * @param artifact The artifact for whose descriptor to determine the error policy, may be {@code null}.
+     * @param context The context in which this request is made, may be {@code null}.
+     */
+    public ArtifactDescriptorPolicyRequest( Artifact artifact, String context )
+    {
+        setArtifact( artifact );
+        setRequestContext( context );
+    }
+
+    /**
+     * Gets the artifact for whose descriptor to determine the error policy.
+     * 
+     * @return The artifact for whose descriptor to determine the error policy or {@code null} if not set.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact for whose descriptor to determine the error policy.
+     * 
+     * @param artifact The artifact for whose descriptor to determine the error policy, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorPolicyRequest setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the context in which this request is made.
+     * 
+     * @return The context, never {@code null}.
+     */
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the context in which this request is made.
+     * 
+     * @param context The context, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorPolicyRequest setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( getArtifact() );
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactDescriptorRequest.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactDescriptorRequest.java
new file mode 100644 (file)
index 0000000..9a1ba65
--- /dev/null
@@ -0,0 +1,181 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A request to read an artifact descriptor.
+ * 
+ * @see RepositorySystem#readArtifactDescriptor(RepositorySystemSession, ArtifactDescriptorRequest)
+ */
+public final class ArtifactDescriptorRequest
+{
+
+    private Artifact artifact;
+
+    private List<RemoteRepository> repositories = Collections.emptyList();
+
+    private String context = "";
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public ArtifactDescriptorRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request with the specified properties.
+     * 
+     * @param artifact The artifact whose descriptor should be read, may be {@code null}.
+     * @param repositories The repositories to resolve the descriptor from, may be {@code null}.
+     * @param context The context in which this request is made, may be {@code null}.
+     */
+    public ArtifactDescriptorRequest( Artifact artifact, List<RemoteRepository> repositories, String context )
+    {
+        setArtifact( artifact );
+        setRepositories( repositories );
+        setRequestContext( context );
+    }
+
+    /**
+     * Gets the artifact whose descriptor shall be read.
+     * 
+     * @return The artifact or {@code null} if not set.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact whose descriptor shall be read. Eventually, a valid request must have an artifact set.
+     * 
+     * @param artifact The artifact, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorRequest setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the repositories to resolve the descriptor from.
+     * 
+     * @return The repositories, never {@code null}.
+     */
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the repositories to resolve the descriptor from.
+     * 
+     * @param repositories The repositories, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorRequest setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories == null )
+        {
+            this.repositories = Collections.emptyList();
+        }
+        else
+        {
+            this.repositories = repositories;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified repository for the resolution of the artifact descriptor.
+     * 
+     * @param repository The repository to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorRequest addRepository( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            if ( this.repositories.isEmpty() )
+            {
+                this.repositories = new ArrayList<RemoteRepository>();
+            }
+            this.repositories.add( repository );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the context in which this request is made.
+     * 
+     * @return The context, never {@code null}.
+     */
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the context in which this request is made.
+     * 
+     * @param context The context, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorRequest setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifact() + " < " + getRepositories();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactDescriptorResult.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactDescriptorResult.java
new file mode 100644 (file)
index 0000000..3de8d5e
--- /dev/null
@@ -0,0 +1,457 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * The result from reading an artifact descriptor.
+ * 
+ * @see RepositorySystem#readArtifactDescriptor(RepositorySystemSession, ArtifactDescriptorRequest)
+ */
+public final class ArtifactDescriptorResult
+{
+
+    private final ArtifactDescriptorRequest request;
+
+    private List<Exception> exceptions;
+
+    private List<Artifact> relocations;
+
+    private Collection<Artifact> aliases;
+
+    private Artifact artifact;
+
+    private ArtifactRepository repository;
+
+    private List<Dependency> dependencies;
+
+    private List<Dependency> managedDependencies;
+
+    private List<RemoteRepository> repositories;
+
+    private Map<String, Object> properties;
+
+    /**
+     * Creates a new result for the specified request.
+     * 
+     * @param request The descriptor request, must not be {@code null}.
+     */
+    public ArtifactDescriptorResult( ArtifactDescriptorRequest request )
+    {
+        if ( request == null )
+        {
+            throw new IllegalArgumentException( "artifact descriptor request has not been specified" );
+        }
+        this.request = request;
+        artifact = request.getArtifact();
+        exceptions = Collections.emptyList();
+        relocations = Collections.emptyList();
+        aliases = Collections.emptyList();
+        dependencies = managedDependencies = Collections.emptyList();
+        repositories = Collections.emptyList();
+        properties = Collections.emptyMap();
+    }
+
+    /**
+     * Gets the descriptor request that was made.
+     * 
+     * @return The descriptor request, never {@code null}.
+     */
+    public ArtifactDescriptorRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the exceptions that occurred while reading the artifact descriptor.
+     * 
+     * @return The exceptions that occurred, never {@code null}.
+     */
+    public List<Exception> getExceptions()
+    {
+        return exceptions;
+    }
+
+    /**
+     * Sets the exceptions that occurred while reading the artifact descriptor.
+     * 
+     * @param exceptions The exceptions that occurred, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setExceptions( List<Exception> exceptions )
+    {
+        if ( exceptions == null )
+        {
+            this.exceptions = Collections.emptyList();
+        }
+        else
+        {
+            this.exceptions = exceptions;
+        }
+        return this;
+    }
+
+    /**
+     * Records the specified exception while reading the artifact descriptor.
+     * 
+     * @param exception The exception to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult addException( Exception exception )
+    {
+        if ( exception != null )
+        {
+            if ( exceptions.isEmpty() )
+            {
+                exceptions = new ArrayList<Exception>();
+            }
+            exceptions.add( exception );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the relocations that were processed to read the artifact descriptor. The returned list denotes the hops that
+     * lead to the final artifact coordinates as given by {@link #getArtifact()}.
+     * 
+     * @return The relocations that were processed, never {@code null}.
+     */
+    public List<Artifact> getRelocations()
+    {
+        return relocations;
+    }
+
+    /**
+     * Sets the relocations that were processed to read the artifact descriptor.
+     * 
+     * @param relocations The relocations that were processed, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setRelocations( List<Artifact> relocations )
+    {
+        if ( relocations == null )
+        {
+            this.relocations = Collections.emptyList();
+        }
+        else
+        {
+            this.relocations = relocations;
+        }
+        return this;
+    }
+
+    /**
+     * Records the specified relocation hop while locating the artifact descriptor.
+     * 
+     * @param artifact The artifact that got relocated, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult addRelocation( Artifact artifact )
+    {
+        if ( artifact != null )
+        {
+            if ( relocations.isEmpty() )
+            {
+                relocations = new ArrayList<Artifact>();
+            }
+            relocations.add( artifact );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the known aliases for this artifact. An alias denotes a different artifact with (almost) the same contents
+     * and can be used to mark a patched rebuild of some other artifact as such, thereby allowing conflict resolution to
+     * consider the patched and the original artifact as a conflict.
+     * 
+     * @return The aliases of the artifact, never {@code null}.
+     */
+    public Collection<Artifact> getAliases()
+    {
+        return aliases;
+    }
+
+    /**
+     * Sets the aliases of the artifact.
+     * 
+     * @param aliases The aliases of the artifact, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setAliases( Collection<Artifact> aliases )
+    {
+        if ( aliases == null )
+        {
+            this.aliases = Collections.emptyList();
+        }
+        else
+        {
+            this.aliases = aliases;
+        }
+        return this;
+    }
+
+    /**
+     * Records the specified alias.
+     * 
+     * @param alias The alias for the artifact, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult addAlias( Artifact alias )
+    {
+        if ( alias != null )
+        {
+            if ( aliases.isEmpty() )
+            {
+                aliases = new ArrayList<Artifact>();
+            }
+            aliases.add( alias );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the artifact whose descriptor was read. This can be a different artifact than originally requested in case
+     * relocations were encountered.
+     * 
+     * @return The artifact after following any relocations, never {@code null}.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact whose descriptor was read.
+     * 
+     * @param artifact The artifact whose descriptor was read, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the repository from which the descriptor was eventually resolved.
+     * 
+     * @return The repository from which the descriptor was resolved or {@code null} if unknown.
+     */
+    public ArtifactRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the repository from which the descriptor was resolved.
+     * 
+     * @param repository The repository from which the descriptor was resolved, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setRepository( ArtifactRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    /**
+     * Gets the list of direct dependencies of the artifact.
+     * 
+     * @return The list of direct dependencies, never {@code null}
+     */
+    public List<Dependency> getDependencies()
+    {
+        return dependencies;
+    }
+
+    /**
+     * Sets the list of direct dependencies of the artifact.
+     * 
+     * @param dependencies The list of direct dependencies, may be {@code null}
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setDependencies( List<Dependency> dependencies )
+    {
+        if ( dependencies == null )
+        {
+            this.dependencies = Collections.emptyList();
+        }
+        else
+        {
+            this.dependencies = dependencies;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified direct dependency.
+     * 
+     * @param dependency The direct dependency to add, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult addDependency( Dependency dependency )
+    {
+        if ( dependency != null )
+        {
+            if ( dependencies.isEmpty() )
+            {
+                dependencies = new ArrayList<Dependency>();
+            }
+            dependencies.add( dependency );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the dependency management information.
+     * 
+     * @return The dependency management information.
+     */
+    public List<Dependency> getManagedDependencies()
+    {
+        return managedDependencies;
+    }
+
+    /**
+     * Sets the dependency management information.
+     * 
+     * @param dependencies The dependency management information, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setManagedDependencies( List<Dependency> dependencies )
+    {
+        if ( dependencies == null )
+        {
+            this.managedDependencies = Collections.emptyList();
+        }
+        else
+        {
+            this.managedDependencies = dependencies;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified managed dependency.
+     * 
+     * @param dependency The managed dependency to add, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult addManagedDependency( Dependency dependency )
+    {
+        if ( dependency != null )
+        {
+            if ( managedDependencies.isEmpty() )
+            {
+                managedDependencies = new ArrayList<Dependency>();
+            }
+            managedDependencies.add( dependency );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the remote repositories listed in the artifact descriptor.
+     * 
+     * @return The remote repositories listed in the artifact descriptor, never {@code null}.
+     */
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the remote repositories listed in the artifact descriptor.
+     * 
+     * @param repositories The remote repositories listed in the artifact descriptor, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories == null )
+        {
+            this.repositories = Collections.emptyList();
+        }
+        else
+        {
+            this.repositories = repositories;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified remote repository.
+     * 
+     * @param repository The remote repository to add, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult addRepository( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            if ( repositories.isEmpty() )
+            {
+                repositories = new ArrayList<RemoteRepository>();
+            }
+            repositories.add( repository );
+        }
+        return this;
+    }
+
+    /**
+     * Gets any additional information about the artifact in form of key-value pairs. <em>Note:</em> Regardless of their
+     * actual type, all property values must be treated as being read-only.
+     * 
+     * @return The additional information about the artifact, never {@code null}.
+     */
+    public Map<String, Object> getProperties()
+    {
+        return properties;
+    }
+
+    /**
+     * Sets any additional information about the artifact in form of key-value pairs.
+     * 
+     * @param properties The additional information about the artifact, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setProperties( Map<String, Object> properties )
+    {
+        if ( properties == null )
+        {
+            this.properties = Collections.emptyMap();
+        }
+        else
+        {
+            this.properties = properties;
+        }
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifact() + " -> " + getDependencies();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactRequest.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactRequest.java
new file mode 100644 (file)
index 0000000..6076ea5
--- /dev/null
@@ -0,0 +1,223 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A request to resolve an artifact.
+ * 
+ * @see RepositorySystem#resolveArtifacts(RepositorySystemSession, java.util.Collection)
+ * @see Artifact#getFile()
+ */
+public final class ArtifactRequest
+{
+
+    private Artifact artifact;
+
+    private DependencyNode node;
+
+    private List<RemoteRepository> repositories = Collections.emptyList();
+
+    private String context = "";
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public ArtifactRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request with the specified properties.
+     * 
+     * @param artifact The artifact to resolve, may be {@code null}.
+     * @param repositories The repositories to resolve the artifact from, may be {@code null}.
+     * @param context The context in which this request is made, may be {@code null}.
+     */
+    public ArtifactRequest( Artifact artifact, List<RemoteRepository> repositories, String context )
+    {
+        setArtifact( artifact );
+        setRepositories( repositories );
+        setRequestContext( context );
+    }
+
+    /**
+     * Creates a request from the specified dependency node.
+     * 
+     * @param node The dependency node to resolve, may be {@code null}.
+     */
+    public ArtifactRequest( DependencyNode node )
+    {
+        setDependencyNode( node );
+        setRepositories( node.getRepositories() );
+        setRequestContext( node.getRequestContext() );
+    }
+
+    /**
+     * Gets the artifact to resolve.
+     * 
+     * @return The artifact to resolve or {@code null}.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact to resolve.
+     * 
+     * @param artifact The artifact to resolve, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactRequest setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the dependency node (if any) for which to resolve the artifact.
+     * 
+     * @return The dependency node to resolve or {@code null} if unknown.
+     */
+    public DependencyNode getDependencyNode()
+    {
+        return node;
+    }
+
+    /**
+     * Sets the dependency node to resolve.
+     * 
+     * @param node The dependency node to resolve, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactRequest setDependencyNode( DependencyNode node )
+    {
+        this.node = node;
+        if ( node != null )
+        {
+            setArtifact( node.getDependency().getArtifact() );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the repositories to resolve the artifact from.
+     * 
+     * @return The repositories, never {@code null}.
+     */
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the repositories to resolve the artifact from.
+     * 
+     * @param repositories The repositories, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactRequest setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories == null )
+        {
+            this.repositories = Collections.emptyList();
+        }
+        else
+        {
+            this.repositories = repositories;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified repository for the resolution.
+     * 
+     * @param repository The repository to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactRequest addRepository( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            if ( this.repositories.isEmpty() )
+            {
+                this.repositories = new ArrayList<RemoteRepository>();
+            }
+            this.repositories.add( repository );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the context in which this request is made.
+     * 
+     * @return The context, never {@code null}.
+     */
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the context in which this request is made.
+     * 
+     * @param context The context, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactRequest setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifact() + " < " + getRepositories();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactResolutionException.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactResolutionException.java
new file mode 100644 (file)
index 0000000..67d0514
--- /dev/null
@@ -0,0 +1,164 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.transfer.ArtifactNotFoundException;
+import org.eclipse.aether.transfer.RepositoryOfflineException;
+
+/**
+ * Thrown in case of a unresolvable artifacts.
+ */
+public class ArtifactResolutionException
+    extends RepositoryException
+{
+
+    private final transient List<ArtifactResult> results;
+
+    /**
+     * Creates a new exception with the specified results.
+     * 
+     * @param results The resolution results at the point the exception occurred, may be {@code null}.
+     */
+    public ArtifactResolutionException( List<ArtifactResult> results )
+    {
+        super( getMessage( results ), getCause( results ) );
+        this.results = ( results != null ) ? results : Collections.<ArtifactResult> emptyList();
+    }
+
+    /**
+     * Creates a new exception with the specified results and detail message.
+     * 
+     * @param results The resolution results at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public ArtifactResolutionException( List<ArtifactResult> results, String message )
+    {
+        super( message, getCause( results ) );
+        this.results = ( results != null ) ? results : Collections.<ArtifactResult> emptyList();
+    }
+
+    /**
+     * Creates a new exception with the specified results, detail message and cause.
+     * 
+     * @param results The resolution results at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public ArtifactResolutionException( List<ArtifactResult> results, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.results = ( results != null ) ? results : Collections.<ArtifactResult> emptyList();
+    }
+
+    /**
+     * Gets the resolution results at the point the exception occurred. Despite being incomplete, callers might want to
+     * use these results to fail gracefully and continue their operation with whatever interim data has been gathered.
+     * 
+     * @return The resolution results or {@code null} if unknown.
+     */
+    public List<ArtifactResult> getResults()
+    {
+        return results;
+    }
+
+    /**
+     * Gets the first result from {@link #getResults()}. This is a convenience method for cases where callers know only
+     * a single result/request is involved.
+     * 
+     * @return The (first) resolution result or {@code null} if none.
+     */
+    public ArtifactResult getResult()
+    {
+        return ( results != null && !results.isEmpty() ) ? results.get( 0 ) : null;
+    }
+
+    private static String getMessage( List<? extends ArtifactResult> results )
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+
+        buffer.append( "The following artifacts could not be resolved: " );
+
+        int unresolved = 0;
+
+        String sep = "";
+        for ( ArtifactResult result : results )
+        {
+            if ( !result.isResolved() )
+            {
+                unresolved++;
+
+                buffer.append( sep );
+                buffer.append( result.getRequest().getArtifact() );
+                sep = ", ";
+            }
+        }
+
+        Throwable cause = getCause( results );
+        if ( cause != null )
+        {
+            if ( unresolved == 1 )
+            {
+                buffer.setLength( 0 );
+                buffer.append( cause.getMessage() );
+            }
+            else
+            {
+                buffer.append( ": " ).append( cause.getMessage() );
+            }
+        }
+
+        return buffer.toString();
+    }
+
+    private static Throwable getCause( List<? extends ArtifactResult> results )
+    {
+        for ( ArtifactResult result : results )
+        {
+            if ( !result.isResolved() )
+            {
+                Throwable notFound = null, offline = null;
+                for ( Throwable t : result.getExceptions() )
+                {
+                    if ( t instanceof ArtifactNotFoundException )
+                    {
+                        if ( notFound == null )
+                        {
+                            notFound = t;
+                        }
+                        if ( offline == null && t.getCause() instanceof RepositoryOfflineException )
+                        {
+                            offline = t;
+                        }
+                    }
+                    else
+                    {
+                        return t;
+                    }
+
+                }
+                if ( offline != null )
+                {
+                    return offline;
+                }
+                if ( notFound != null )
+                {
+                    return notFound;
+                }
+            }
+        }
+        return null;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactResult.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ArtifactResult.java
new file mode 100644 (file)
index 0000000..106ffe0
--- /dev/null
@@ -0,0 +1,179 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.transfer.ArtifactNotFoundException;
+
+/**
+ * The result of an artifact resolution request.
+ * 
+ * @see RepositorySystem#resolveArtifacts(RepositorySystemSession, java.util.Collection)
+ * @see Artifact#getFile()
+ */
+public final class ArtifactResult
+{
+
+    private final ArtifactRequest request;
+
+    private List<Exception> exceptions;
+
+    private Artifact artifact;
+
+    private ArtifactRepository repository;
+
+    /**
+     * Creates a new result for the specified request.
+     * 
+     * @param request The resolution request, must not be {@code null}.
+     */
+    public ArtifactResult( ArtifactRequest request )
+    {
+        if ( request == null )
+        {
+            throw new IllegalArgumentException( "resolution request has not been specified" );
+        }
+        this.request = request;
+        exceptions = Collections.emptyList();
+    }
+
+    /**
+     * Gets the resolution request that was made.
+     * 
+     * @return The resolution request, never {@code null}.
+     */
+    public ArtifactRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the resolved artifact (if any). Use {@link #getExceptions()} to query the errors that occurred while trying
+     * to resolve the artifact.
+     * 
+     * @return The resolved artifact or {@code null} if the resolution failed.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the resolved artifact.
+     * 
+     * @param artifact The resolved artifact, may be {@code null} if the resolution failed.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactResult setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the exceptions that occurred while resolving the artifact. Note that this list can be non-empty even if the
+     * artifact was successfully resolved, e.g. when one of the contacted remote repositories didn't contain the
+     * artifact but a later repository eventually contained it.
+     * 
+     * @return The exceptions that occurred, never {@code null}.
+     * @see #isResolved()
+     */
+    public List<Exception> getExceptions()
+    {
+        return exceptions;
+    }
+
+    /**
+     * Records the specified exception while resolving the artifact.
+     * 
+     * @param exception The exception to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactResult addException( Exception exception )
+    {
+        if ( exception != null )
+        {
+            if ( exceptions.isEmpty() )
+            {
+                exceptions = new ArrayList<Exception>();
+            }
+            exceptions.add( exception );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the repository from which the artifact was eventually resolved. Note that successive resolutions of the same
+     * artifact might yield different results if the employed local repository does not track the origin of an artifact.
+     * 
+     * @return The repository from which the artifact was resolved or {@code null} if unknown.
+     */
+    public ArtifactRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the repository from which the artifact was resolved.
+     * 
+     * @param repository The repository from which the artifact was resolved, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactResult setRepository( ArtifactRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    /**
+     * Indicates whether the requested artifact was resolved. Note that the artifact might have been successfully
+     * resolved despite {@link #getExceptions()} indicating transfer errors while trying to fetch the artifact from some
+     * of the specified remote repositories.
+     * 
+     * @return {@code true} if the artifact was resolved, {@code false} otherwise.
+     * @see Artifact#getFile()
+     */
+    public boolean isResolved()
+    {
+        return getArtifact() != null && getArtifact().getFile() != null;
+    }
+
+    /**
+     * Indicates whether the requested artifact is not present in any of the specified repositories.
+     * 
+     * @return {@code true} if the artifact is not present in any repository, {@code false} otherwise.
+     */
+    public boolean isMissing()
+    {
+        for ( Exception e : getExceptions() )
+        {
+            if ( !( e instanceof ArtifactNotFoundException ) )
+            {
+                return false;
+            }
+        }
+        return !isResolved();
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifact() + " < " + getRepository();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/DependencyRequest.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/DependencyRequest.java
new file mode 100644 (file)
index 0000000..f55aff7
--- /dev/null
@@ -0,0 +1,178 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.collection.CollectRequest;
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * A request to resolve transitive dependencies. This request can either be supplied with a {@link CollectRequest} to
+ * calculate the transitive dependencies or with an already resolved dependency graph.
+ * 
+ * @see RepositorySystem#resolveDependencies(RepositorySystemSession, DependencyRequest)
+ * @see Artifact#getFile()
+ */
+public final class DependencyRequest
+{
+
+    private DependencyNode root;
+
+    private CollectRequest collectRequest;
+
+    private DependencyFilter filter;
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request. Note that either {@link #setRoot(DependencyNode)} or
+     * {@link #setCollectRequest(CollectRequest)} must eventually be called to create a valid request.
+     */
+    public DependencyRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request for the specified dependency graph and with the given resolution filter.
+     * 
+     * @param node The root node of the dependency graph whose artifacts should be resolved, may be {@code null}.
+     * @param filter The resolution filter to use, may be {@code null}.
+     */
+    public DependencyRequest( DependencyNode node, DependencyFilter filter )
+    {
+        setRoot( node );
+        setFilter( filter );
+    }
+
+    /**
+     * Creates a request for the specified collect request and with the given resolution filter.
+     * 
+     * @param request The collect request used to calculate the dependency graph whose artifacts should be resolved, may
+     *            be {@code null}.
+     * @param filter The resolution filter to use, may be {@code null}.
+     */
+    public DependencyRequest( CollectRequest request, DependencyFilter filter )
+    {
+        setCollectRequest( request );
+        setFilter( filter );
+    }
+
+    /**
+     * Gets the root node of the dependency graph whose artifacts should be resolved.
+     * 
+     * @return The root node of the dependency graph or {@code null} if none.
+     */
+    public DependencyNode getRoot()
+    {
+        return root;
+    }
+
+    /**
+     * Sets the root node of the dependency graph whose artifacts should be resolved. When this request is processed,
+     * the nodes of the given dependency graph will be updated to refer to the resolved artifacts. Eventually, either
+     * {@link #setRoot(DependencyNode)} or {@link #setCollectRequest(CollectRequest)} must be called to create a valid
+     * request.
+     * 
+     * @param root The root node of the dependency graph, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DependencyRequest setRoot( DependencyNode root )
+    {
+        this.root = root;
+        return this;
+    }
+
+    /**
+     * Gets the collect request used to calculate the dependency graph whose artifacts should be resolved.
+     * 
+     * @return The collect request or {@code null} if none.
+     */
+    public CollectRequest getCollectRequest()
+    {
+        return collectRequest;
+    }
+
+    /**
+     * Sets the collect request used to calculate the dependency graph whose artifacts should be resolved. Eventually,
+     * either {@link #setRoot(DependencyNode)} or {@link #setCollectRequest(CollectRequest)} must be called to create a
+     * valid request. If this request is supplied with a dependency node via {@link #setRoot(DependencyNode)}, the
+     * collect request is ignored.
+     * 
+     * @param collectRequest The collect request, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DependencyRequest setCollectRequest( CollectRequest collectRequest )
+    {
+        this.collectRequest = collectRequest;
+        return this;
+    }
+
+    /**
+     * Gets the resolution filter used to select which artifacts of the dependency graph should be resolved.
+     * 
+     * @return The resolution filter or {@code null} to resolve all artifacts of the dependency graph.
+     */
+    public DependencyFilter getFilter()
+    {
+        return filter;
+    }
+
+    /**
+     * Sets the resolution filter used to select which artifacts of the dependency graph should be resolved. For
+     * example, use this filter to restrict resolution to dependencies of a certain scope.
+     * 
+     * @param filter The resolution filter, may be {@code null} to resolve all artifacts of the dependency graph.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DependencyRequest setFilter( DependencyFilter filter )
+    {
+        this.filter = filter;
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DependencyRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        if ( root != null )
+        {
+            return String.valueOf( root );
+        }
+        return String.valueOf( collectRequest );
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/DependencyResolutionException.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/DependencyResolutionException.java
new file mode 100644 (file)
index 0000000..27d9bb2
--- /dev/null
@@ -0,0 +1,74 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of a unresolvable dependencies.
+ */
+public class DependencyResolutionException
+    extends RepositoryException
+{
+
+    private final transient DependencyResult result;
+
+    /**
+     * Creates a new exception with the specified result and cause.
+     * 
+     * @param result The dependency result at the point the exception occurred, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public DependencyResolutionException( DependencyResult result, Throwable cause )
+    {
+        super( getMessage( cause ), cause );
+        this.result = result;
+    }
+
+    /**
+     * Creates a new exception with the specified result, detail message and cause.
+     * 
+     * @param result The dependency result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public DependencyResolutionException( DependencyResult result, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.result = result;
+    }
+
+    private static String getMessage( Throwable cause )
+    {
+        String msg = null;
+        if ( cause != null )
+        {
+            msg = cause.getMessage();
+        }
+        if ( msg == null || msg.length() <= 0 )
+        {
+            msg = "Could not resolve transitive dependencies";
+        }
+        return msg;
+    }
+
+    /**
+     * Gets the dependency result at the point the exception occurred. Despite being incomplete, callers might want to
+     * use this result to fail gracefully and continue their operation with whatever interim data has been gathered.
+     * 
+     * @return The dependency result or {@code null} if unknown.
+     */
+    public DependencyResult getResult()
+    {
+        return result;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/DependencyResult.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/DependencyResult.java
new file mode 100644 (file)
index 0000000..3cc8d3a
--- /dev/null
@@ -0,0 +1,186 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.graph.DependencyCycle;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * The result of a dependency resolution request.
+ * 
+ * @see RepositorySystem#resolveDependencies(RepositorySystemSession, DependencyRequest)
+ */
+public final class DependencyResult
+{
+
+    private final DependencyRequest request;
+
+    private DependencyNode root;
+
+    private List<DependencyCycle> cycles;
+
+    private List<Exception> collectExceptions;
+
+    private List<ArtifactResult> artifactResults;
+
+    /**
+     * Creates a new result for the specified request.
+     * 
+     * @param request The resolution request, must not be {@code null}.
+     */
+    public DependencyResult( DependencyRequest request )
+    {
+        if ( request == null )
+        {
+            throw new IllegalArgumentException( "dependency request has not been specified" );
+        }
+        this.request = request;
+        root = request.getRoot();
+        cycles = Collections.emptyList();
+        collectExceptions = Collections.emptyList();
+        artifactResults = Collections.emptyList();
+    }
+
+    /**
+     * Gets the resolution request that was made.
+     * 
+     * @return The resolution request, never {@code null}.
+     */
+    public DependencyRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the root node of the resolved dependency graph. Note that this dependency graph might be
+     * incomplete/unfinished in case of {@link #getCollectExceptions()} indicating errors during its calculation.
+     * 
+     * @return The root node of the resolved dependency graph or {@code null} if none.
+     */
+    public DependencyNode getRoot()
+    {
+        return root;
+    }
+
+    /**
+     * Sets the root node of the resolved dependency graph.
+     * 
+     * @param root The root node of the resolved dependency graph, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public DependencyResult setRoot( DependencyNode root )
+    {
+        this.root = root;
+        return this;
+    }
+
+    /**
+     * Gets the dependency cycles that were encountered while building the dependency graph. Note that dependency cycles
+     * will only be reported here if the underlying request was created from a
+     * {@link org.eclipse.aether.collection.CollectRequest CollectRequest}. If the underlying {@link DependencyRequest}
+     * was created from an existing dependency graph, information about cycles will not be available in this result.
+     * 
+     * @return The dependency cycles in the (raw) graph, never {@code null}.
+     */
+    public List<DependencyCycle> getCycles()
+    {
+        return cycles;
+    }
+
+    /**
+     * Records the specified dependency cycles while building the dependency graph.
+     * 
+     * @param cycles The dependency cycles to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public DependencyResult setCycles( List<DependencyCycle> cycles )
+    {
+        if ( cycles == null )
+        {
+            this.cycles = Collections.emptyList();
+        }
+        else
+        {
+            this.cycles = cycles;
+        }
+        return this;
+    }
+
+    /**
+     * Gets the exceptions that occurred while building the dependency graph.
+     * 
+     * @return The exceptions that occurred, never {@code null}.
+     */
+    public List<Exception> getCollectExceptions()
+    {
+        return collectExceptions;
+    }
+
+    /**
+     * Records the specified exceptions while building the dependency graph.
+     * 
+     * @param exceptions The exceptions to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public DependencyResult setCollectExceptions( List<Exception> exceptions )
+    {
+        if ( exceptions == null )
+        {
+            this.collectExceptions = Collections.emptyList();
+        }
+        else
+        {
+            this.collectExceptions = exceptions;
+        }
+        return this;
+    }
+
+    /**
+     * Gets the resolution results for the dependency artifacts that matched {@link DependencyRequest#getFilter()}.
+     * 
+     * @return The resolution results for the dependency artifacts, never {@code null}.
+     */
+    public List<ArtifactResult> getArtifactResults()
+    {
+        return artifactResults;
+    }
+
+    /**
+     * Sets the resolution results for the artifacts that matched {@link DependencyRequest#getFilter()}.
+     * 
+     * @param results The resolution results for the artifacts, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public DependencyResult setArtifactResults( List<ArtifactResult> results )
+    {
+        if ( results == null )
+        {
+            this.artifactResults = Collections.emptyList();
+        }
+        else
+        {
+            this.artifactResults = results;
+        }
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( artifactResults );
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/MetadataRequest.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/MetadataRequest.java
new file mode 100644 (file)
index 0000000..2f6c5f1
--- /dev/null
@@ -0,0 +1,223 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A request to resolve metadata from either a remote repository or the local repository.
+ * 
+ * @see RepositorySystem#resolveMetadata(RepositorySystemSession, java.util.Collection)
+ * @see Metadata#getFile()
+ */
+public final class MetadataRequest
+{
+
+    private Metadata metadata;
+
+    private RemoteRepository repository;
+
+    private String context = "";
+
+    private boolean deleteLocalCopyIfMissing;
+
+    private boolean favorLocalRepository;
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public MetadataRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request to resolve the specified metadata from the local repository.
+     * 
+     * @param metadata The metadata to resolve, may be {@code null}.
+     */
+    public MetadataRequest( Metadata metadata )
+    {
+        setMetadata( metadata );
+    }
+
+    /**
+     * Creates a request with the specified properties.
+     * 
+     * @param metadata The metadata to resolve, may be {@code null}.
+     * @param repository The repository to resolve the metadata from, may be {@code null} to resolve from the local
+     *            repository.
+     * @param context The context in which this request is made, may be {@code null}.
+     */
+    public MetadataRequest( Metadata metadata, RemoteRepository repository, String context )
+    {
+        setMetadata( metadata );
+        setRepository( repository );
+        setRequestContext( context );
+    }
+
+    /**
+     * Gets the metadata to resolve.
+     * 
+     * @return The metadata or {@code null} if not set.
+     */
+    public Metadata getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Sets the metadata to resolve.
+     * 
+     * @param metadata The metadata, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public MetadataRequest setMetadata( Metadata metadata )
+    {
+        this.metadata = metadata;
+        return this;
+    }
+
+    /**
+     * Gets the repository from which the metadata should be resolved.
+     * 
+     * @return The repository or {@code null} to resolve from the local repository.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the repository from which the metadata should be resolved.
+     * 
+     * @param repository The repository, may be {@code null} to resolve from the local repository.
+     * @return This request for chaining, never {@code null}.
+     */
+    public MetadataRequest setRepository( RemoteRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    /**
+     * Gets the context in which this request is made.
+     * 
+     * @return The context, never {@code null}.
+     */
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the context in which this request is made.
+     * 
+     * @param context The context, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public MetadataRequest setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Indicates whether the locally cached copy of the metadata should be removed if the corresponding file does not
+     * exist (any more) in the remote repository.
+     * 
+     * @return {@code true} if locally cached metadata should be deleted if no corresponding remote file exists,
+     *         {@code false} to keep the local copy.
+     */
+    public boolean isDeleteLocalCopyIfMissing()
+    {
+        return deleteLocalCopyIfMissing;
+    }
+
+    /**
+     * Controls whether the locally cached copy of the metadata should be removed if the corresponding file does not
+     * exist (any more) in the remote repository.
+     * 
+     * @param deleteLocalCopyIfMissing {@code true} if locally cached metadata should be deleted if no corresponding
+     *            remote file exists, {@code false} to keep the local copy.
+     * @return This request for chaining, never {@code null}.
+     */
+    public MetadataRequest setDeleteLocalCopyIfMissing( boolean deleteLocalCopyIfMissing )
+    {
+        this.deleteLocalCopyIfMissing = deleteLocalCopyIfMissing;
+        return this;
+    }
+
+    /**
+     * Indicates whether the metadata resolution should be suppressed if the corresponding metadata of the local
+     * repository is up-to-date according to the update policy of the remote repository. In this case, the metadata
+     * resolution will even be suppressed if no local copy of the remote metadata exists yet.
+     * 
+     * @return {@code true} to suppress resolution of remote metadata if the corresponding metadata of the local
+     *         repository is up-to-date, {@code false} to resolve the remote metadata normally according to the update
+     *         policy.
+     */
+    public boolean isFavorLocalRepository()
+    {
+        return favorLocalRepository;
+    }
+
+    /**
+     * Controls resolution of remote metadata when already corresponding metadata of the local repository exists. In
+     * cases where the local repository's metadata is sufficient and going to be preferred, resolution of the remote
+     * metadata can be suppressed to avoid unnecessary network access.
+     * 
+     * @param favorLocalRepository {@code true} to suppress resolution of remote metadata if the corresponding metadata
+     *            of the local repository is up-to-date, {@code false} to resolve the remote metadata normally according
+     *            to the update policy.
+     * @return This request for chaining, never {@code null}.
+     */
+    public MetadataRequest setFavorLocalRepository( boolean favorLocalRepository )
+    {
+        this.favorLocalRepository = favorLocalRepository;
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public MetadataRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getMetadata() + " < " + getRepository();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/MetadataResult.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/MetadataResult.java
new file mode 100644 (file)
index 0000000..3e5a7b8
--- /dev/null
@@ -0,0 +1,157 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.transfer.MetadataNotFoundException;
+
+/**
+ * The result of a metadata resolution request.
+ * 
+ * @see RepositorySystem#resolveMetadata(RepositorySystemSession, java.util.Collection)
+ */
+public final class MetadataResult
+{
+
+    private final MetadataRequest request;
+
+    private Exception exception;
+
+    private boolean updated;
+
+    private Metadata metadata;
+
+    /**
+     * Creates a new result for the specified request.
+     * 
+     * @param request The resolution request, must not be {@code null}.
+     */
+    public MetadataResult( MetadataRequest request )
+    {
+        if ( request == null )
+        {
+            throw new IllegalArgumentException( "metadata request has not been specified" );
+        }
+        this.request = request;
+    }
+
+    /**
+     * Gets the resolution request that was made.
+     * 
+     * @return The resolution request, never {@code null}.
+     */
+    public MetadataRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the resolved metadata (if any).
+     * 
+     * @return The resolved metadata or {@code null} if the resolution failed.
+     */
+    public Metadata getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Sets the resolved metadata.
+     * 
+     * @param metadata The resolved metadata, may be {@code null} if the resolution failed.
+     * @return This result for chaining, never {@code null}.
+     */
+    public MetadataResult setMetadata( Metadata metadata )
+    {
+        this.metadata = metadata;
+        return this;
+    }
+
+    /**
+     * Records the specified exception while resolving the metadata.
+     * 
+     * @param exception The exception to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public MetadataResult setException( Exception exception )
+    {
+        this.exception = exception;
+        return this;
+    }
+
+    /**
+     * Gets the exception that occurred while resolving the metadata.
+     * 
+     * @return The exception that occurred or {@code null} if none.
+     */
+    public Exception getException()
+    {
+        return exception;
+    }
+
+    /**
+     * Sets the updated flag for the metadata.
+     * 
+     * @param updated {@code true} if the metadata was actually fetched from the remote repository during the
+     *            resolution, {@code false} if the metadata was resolved from a locally cached copy.
+     * @return This result for chaining, never {@code null}.
+     */
+    public MetadataResult setUpdated( boolean updated )
+    {
+        this.updated = updated;
+        return this;
+    }
+
+    /**
+     * Indicates whether the metadata was actually fetched from the remote repository or resolved from the local cache.
+     * If metadata has been locally cached during a previous resolution request and this local copy is still up-to-date
+     * according to the remote repository's update policy, no remote access is made.
+     * 
+     * @return {@code true} if the metadata was actually fetched from the remote repository during the resolution,
+     *         {@code false} if the metadata was resolved from a locally cached copy.
+     */
+    public boolean isUpdated()
+    {
+        return updated;
+    }
+
+    /**
+     * Indicates whether the requested metadata was resolved. Note that the metadata might have been successfully
+     * resolved (from the local cache) despite {@link #getException()} indicating a transfer error while trying to
+     * refetch the metadata from the remote repository.
+     * 
+     * @return {@code true} if the metadata was resolved, {@code false} otherwise.
+     * @see Metadata#getFile()
+     */
+    public boolean isResolved()
+    {
+        return getMetadata() != null && getMetadata().getFile() != null;
+    }
+
+    /**
+     * Indicates whether the requested metadata is not present in the remote repository.
+     * 
+     * @return {@code true} if the metadata is not present in the remote repository, {@code false} otherwise.
+     */
+    public boolean isMissing()
+    {
+        return getException() instanceof MetadataNotFoundException;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getMetadata() + ( isUpdated() ? " (updated)" : " (cached)" );
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ResolutionErrorPolicy.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ResolutionErrorPolicy.java
new file mode 100644 (file)
index 0000000..50d1a01
--- /dev/null
@@ -0,0 +1,73 @@
+/*******************************************************************************
+ * Copyright (c) 2012, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * Controls the caching of resolution errors for artifacts/metadata from remote repositories. If caching is enabled for
+ * a given resource, a marker will be set (usually somewhere in the local repository) to suppress repeated resolution
+ * attempts for the broken resource, thereby avoiding expensive but useless network IO. The error marker is considered
+ * stale once the repository's update policy has expired at which point a future resolution attempt will be allowed.
+ * Error caching considers the current network settings such that fixes to the configuration like authentication or
+ * proxy automatically trigger revalidation with the remote side regardless of the time elapsed since the previous
+ * resolution error.
+ * 
+ * @see RepositorySystemSession#getResolutionErrorPolicy()
+ */
+public interface ResolutionErrorPolicy
+{
+
+    /**
+     * Bit mask indicating that resolution errors should not be cached in the local repository. This forces the system
+     * to always query the remote repository for locally missing artifacts/metadata.
+     */
+    int CACHE_DISABLED = 0x00;
+
+    /**
+     * Bit flag indicating whether missing artifacts/metadata should be cached in the local repository. If caching is
+     * enabled, resolution will not be reattempted until the update policy for the affected resource has expired.
+     */
+    int CACHE_NOT_FOUND = 0x01;
+
+    /**
+     * Bit flag indicating whether connectivity/transfer errors (e.g. unreachable host, bad authentication) should be
+     * cached in the local repository. If caching is enabled, resolution will not be reattempted until the update policy
+     * for the affected resource has expired.
+     */
+    int CACHE_TRANSFER_ERROR = 0x02;
+
+    /**
+     * Bit mask indicating that all resolution errors should be cached in the local repository.
+     */
+    int CACHE_ALL = CACHE_NOT_FOUND | CACHE_TRANSFER_ERROR;
+
+    /**
+     * Gets the error policy for an artifact.
+     * 
+     * @param session The repository session during which the policy is determined, must not be {@code null}.
+     * @param request The policy request holding further details, must not be {@code null}.
+     * @return The bit mask describing the desired error policy.
+     */
+    int getArtifactPolicy( RepositorySystemSession session, ResolutionErrorPolicyRequest<Artifact> request );
+
+    /**
+     * Gets the error policy for some metadata.
+     * 
+     * @param session The repository session during which the policy is determined, must not be {@code null}.
+     * @param request The policy request holding further details, must not be {@code null}.
+     * @return The bit mask describing the desired error policy.
+     */
+    int getMetadataPolicy( RepositorySystemSession session, ResolutionErrorPolicyRequest<Metadata> request );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ResolutionErrorPolicyRequest.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/ResolutionErrorPolicyRequest.java
new file mode 100644 (file)
index 0000000..6d05cf3
--- /dev/null
@@ -0,0 +1,98 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A query for the resolution error policy for a given artifact/metadata.
+ * 
+ * @param <T> The type of the affected repository item (artifact or metadata).
+ * @see ResolutionErrorPolicy
+ */
+public final class ResolutionErrorPolicyRequest<T>
+{
+
+    private T item;
+
+    private RemoteRepository repository;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public ResolutionErrorPolicyRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request for the specified artifact/metadata and remote repository.
+     * 
+     * @param item The artifact/metadata for which to determine the error policy, may be {@code null}.
+     * @param repository The repository from which the resolution is attempted, may be {@code null}.
+     */
+    public ResolutionErrorPolicyRequest( T item, RemoteRepository repository )
+    {
+        setItem( item );
+        setRepository( repository );
+    }
+
+    /**
+     * Gets the artifact/metadata for which to determine the error policy.
+     * 
+     * @return The artifact/metadata for which to determine the error policy or {@code null} if not set.
+     */
+    public T getItem()
+    {
+        return item;
+    }
+
+    /**
+     * Sets the artifact/metadata for which to determine the error policy.
+     * 
+     * @param item The artifact/metadata for which to determine the error policy, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ResolutionErrorPolicyRequest<T> setItem( T item )
+    {
+        this.item = item;
+        return this;
+    }
+
+    /**
+     * Gets the remote repository from which the resolution of the artifact/metadata is attempted.
+     * 
+     * @return The involved remote repository or {@code null} if not set.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the remote repository from which the resolution of the artifact/metadata is attempted.
+     * 
+     * @param repository The repository from which the resolution is attempted, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ResolutionErrorPolicyRequest<T> setRepository( RemoteRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getItem() + " < " + getRepository();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionRangeRequest.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionRangeRequest.java
new file mode 100644 (file)
index 0000000..b40feb6
--- /dev/null
@@ -0,0 +1,181 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A request to resolve a version range.
+ * 
+ * @see RepositorySystem#resolveVersionRange(RepositorySystemSession, VersionRangeRequest)
+ */
+public final class VersionRangeRequest
+{
+
+    private Artifact artifact;
+
+    private List<RemoteRepository> repositories = Collections.emptyList();
+
+    private String context = "";
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public VersionRangeRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request with the specified properties.
+     * 
+     * @param artifact The artifact whose version range should be resolved, may be {@code null}.
+     * @param repositories The repositories to resolve the version from, may be {@code null}.
+     * @param context The context in which this request is made, may be {@code null}.
+     */
+    public VersionRangeRequest( Artifact artifact, List<RemoteRepository> repositories, String context )
+    {
+        setArtifact( artifact );
+        setRepositories( repositories );
+        setRequestContext( context );
+    }
+
+    /**
+     * Gets the artifact whose version range shall be resolved.
+     * 
+     * @return The artifact or {@code null} if not set.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact whose version range shall be resolved.
+     * 
+     * @param artifact The artifact, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRangeRequest setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the repositories to resolve the version range from.
+     * 
+     * @return The repositories, never {@code null}.
+     */
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the repositories to resolve the version range from.
+     * 
+     * @param repositories The repositories, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRangeRequest setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories == null )
+        {
+            this.repositories = Collections.emptyList();
+        }
+        else
+        {
+            this.repositories = repositories;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified repository for the resolution.
+     * 
+     * @param repository The repository to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRangeRequest addRepository( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            if ( this.repositories.isEmpty() )
+            {
+                this.repositories = new ArrayList<RemoteRepository>();
+            }
+            this.repositories.add( repository );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the context in which this request is made.
+     * 
+     * @return The context, never {@code null}.
+     */
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the context in which this request is made.
+     * 
+     * @param context The context, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRangeRequest setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRangeRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifact() + " < " + getRepositories();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionRangeResolutionException.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionRangeResolutionException.java
new file mode 100644 (file)
index 0000000..6e62e3f
--- /dev/null
@@ -0,0 +1,96 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of an unparseable or unresolvable version range.
+ */
+public class VersionRangeResolutionException
+    extends RepositoryException
+{
+
+    private final transient VersionRangeResult result;
+
+    /**
+     * Creates a new exception with the specified result.
+     * 
+     * @param result The version range result at the point the exception occurred, may be {@code null}.
+     */
+    public VersionRangeResolutionException( VersionRangeResult result )
+    {
+        super( getMessage( result ), getCause( result ) );
+        this.result = result;
+    }
+
+    private static String getMessage( VersionRangeResult result )
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+        buffer.append( "Failed to resolve version range" );
+        if ( result != null )
+        {
+            buffer.append( " for " ).append( result.getRequest().getArtifact() );
+            if ( !result.getExceptions().isEmpty() )
+            {
+                buffer.append( ": " ).append( result.getExceptions().iterator().next().getMessage() );
+            }
+        }
+        return buffer.toString();
+    }
+
+    private static Throwable getCause( VersionRangeResult result )
+    {
+        Throwable cause = null;
+        if ( result != null && !result.getExceptions().isEmpty() )
+        {
+            cause = result.getExceptions().get( 0 );
+        }
+        return cause;
+    }
+
+    /**
+     * Creates a new exception with the specified result and detail message.
+     * 
+     * @param result The version range result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public VersionRangeResolutionException( VersionRangeResult result, String message )
+    {
+        super( message );
+        this.result = result;
+    }
+
+    /**
+     * Creates a new exception with the specified result, detail message and cause.
+     * 
+     * @param result The version range result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public VersionRangeResolutionException( VersionRangeResult result, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.result = result;
+    }
+
+    /**
+     * Gets the version range result at the point the exception occurred. Despite being incomplete, callers might want
+     * to use this result to fail gracefully and continue their operation with whatever interim data has been gathered.
+     * 
+     * @return The version range result or {@code null} if unknown.
+     */
+    public VersionRangeResult getResult()
+    {
+        return result;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionRangeResult.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionRangeResult.java
new file mode 100644 (file)
index 0000000..fd233f1
--- /dev/null
@@ -0,0 +1,231 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+
+/**
+ * The result of a version range resolution request.
+ * 
+ * @see RepositorySystem#resolveVersionRange(RepositorySystemSession, VersionRangeRequest)
+ */
+public final class VersionRangeResult
+{
+
+    private final VersionRangeRequest request;
+
+    private List<Exception> exceptions;
+
+    private List<Version> versions;
+
+    private Map<Version, ArtifactRepository> repositories;
+
+    private VersionConstraint versionConstraint;
+
+    /**
+     * Creates a new result for the specified request.
+     * 
+     * @param request The resolution request, must not be {@code null}.
+     */
+    public VersionRangeResult( VersionRangeRequest request )
+    {
+        if ( request == null )
+        {
+            throw new IllegalArgumentException( "version range request has not been specified" );
+        }
+        this.request = request;
+        exceptions = Collections.emptyList();
+        versions = Collections.emptyList();
+        repositories = Collections.emptyMap();
+    }
+
+    /**
+     * Gets the resolution request that was made.
+     * 
+     * @return The resolution request, never {@code null}.
+     */
+    public VersionRangeRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the exceptions that occurred while resolving the version range.
+     * 
+     * @return The exceptions that occurred, never {@code null}.
+     */
+    public List<Exception> getExceptions()
+    {
+        return exceptions;
+    }
+
+    /**
+     * Records the specified exception while resolving the version range.
+     * 
+     * @param exception The exception to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public VersionRangeResult addException( Exception exception )
+    {
+        if ( exception != null )
+        {
+            if ( exceptions.isEmpty() )
+            {
+                exceptions = new ArrayList<Exception>();
+            }
+            exceptions.add( exception );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the versions (in ascending order) that matched the requested range.
+     * 
+     * @return The matching versions (if any), never {@code null}.
+     */
+    public List<Version> getVersions()
+    {
+        return versions;
+    }
+
+    /**
+     * Adds the specified version to the result. Note that versions must be added in ascending order.
+     * 
+     * @param version The version to add, must not be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public VersionRangeResult addVersion( Version version )
+    {
+        if ( versions.isEmpty() )
+        {
+            versions = new ArrayList<Version>();
+        }
+        versions.add( version );
+        return this;
+    }
+
+    /**
+     * Sets the versions (in ascending order) matching the requested range.
+     * 
+     * @param versions The matching versions, may be empty or {@code null} if none.
+     * @return This result for chaining, never {@code null}.
+     */
+    public VersionRangeResult setVersions( List<Version> versions )
+    {
+        if ( versions == null )
+        {
+            this.versions = Collections.emptyList();
+        }
+        else
+        {
+            this.versions = versions;
+        }
+        return this;
+    }
+
+    /**
+     * Gets the lowest version matching the requested range.
+     * 
+     * @return The lowest matching version or {@code null} if no versions matched the requested range.
+     */
+    public Version getLowestVersion()
+    {
+        if ( versions.isEmpty() )
+        {
+            return null;
+        }
+        return versions.get( 0 );
+    }
+
+    /**
+     * Gets the highest version matching the requested range.
+     * 
+     * @return The highest matching version or {@code null} if no versions matched the requested range.
+     */
+    public Version getHighestVersion()
+    {
+        if ( versions.isEmpty() )
+        {
+            return null;
+        }
+        return versions.get( versions.size() - 1 );
+    }
+
+    /**
+     * Gets the repository from which the specified version was resolved.
+     * 
+     * @param version The version whose source repository should be retrieved, must not be {@code null}.
+     * @return The repository from which the version was resolved or {@code null} if unknown.
+     */
+    public ArtifactRepository getRepository( Version version )
+    {
+        return repositories.get( version );
+    }
+
+    /**
+     * Records the repository from which the specified version was resolved
+     * 
+     * @param version The version whose source repository is to be recorded, must not be {@code null}.
+     * @param repository The repository from which the version was resolved, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public VersionRangeResult setRepository( Version version, ArtifactRepository repository )
+    {
+        if ( repository != null )
+        {
+            if ( repositories.isEmpty() )
+            {
+                repositories = new HashMap<Version, ArtifactRepository>();
+            }
+            repositories.put( version, repository );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the version constraint that was parsed from the artifact's version string.
+     * 
+     * @return The parsed version constraint or {@code null}.
+     */
+    public VersionConstraint getVersionConstraint()
+    {
+        return versionConstraint;
+    }
+
+    /**
+     * Sets the version constraint that was parsed from the artifact's version string.
+     * 
+     * @param versionConstraint The parsed version constraint, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public VersionRangeResult setVersionConstraint( VersionConstraint versionConstraint )
+    {
+        this.versionConstraint = versionConstraint;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( repositories );
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionRequest.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionRequest.java
new file mode 100644 (file)
index 0000000..e18701b
--- /dev/null
@@ -0,0 +1,181 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A request to resolve a metaversion.
+ * 
+ * @see RepositorySystem#resolveVersion(RepositorySystemSession, VersionRequest)
+ */
+public final class VersionRequest
+{
+
+    private Artifact artifact;
+
+    private List<RemoteRepository> repositories = Collections.emptyList();
+
+    private String context = "";
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public VersionRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request with the specified properties.
+     * 
+     * @param artifact The artifact whose (meta-)version should be resolved, may be {@code null}.
+     * @param repositories The repositories to resolve the version from, may be {@code null}.
+     * @param context The context in which this request is made, may be {@code null}.
+     */
+    public VersionRequest( Artifact artifact, List<RemoteRepository> repositories, String context )
+    {
+        setArtifact( artifact );
+        setRepositories( repositories );
+        setRequestContext( context );
+    }
+
+    /**
+     * Gets the artifact whose (meta-)version shall be resolved.
+     * 
+     * @return The artifact or {@code null} if not set.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact whose (meta-)version shall be resolved.
+     * 
+     * @param artifact The artifact, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRequest setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the repositories to resolve the version from.
+     * 
+     * @return The repositories, never {@code null}.
+     */
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the repositories to resolve the version from.
+     * 
+     * @param repositories The repositories, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRequest setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories == null )
+        {
+            this.repositories = Collections.emptyList();
+        }
+        else
+        {
+            this.repositories = repositories;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified repository for the resolution.
+     * 
+     * @param repository The repository to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRequest addRepository( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            if ( this.repositories.isEmpty() )
+            {
+                this.repositories = new ArrayList<RemoteRepository>();
+            }
+            this.repositories.add( repository );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the context in which this request is made.
+     * 
+     * @return The context, never {@code null}.
+     */
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the context in which this request is made.
+     * 
+     * @param context The context, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRequest setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifact() + " < " + getRepositories();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionResolutionException.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionResolutionException.java
new file mode 100644 (file)
index 0000000..25f381e
--- /dev/null
@@ -0,0 +1,96 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of an unresolvable metaversion.
+ */
+public class VersionResolutionException
+    extends RepositoryException
+{
+
+    private final transient VersionResult result;
+
+    /**
+     * Creates a new exception with the specified result.
+     * 
+     * @param result The version result at the point the exception occurred, may be {@code null}.
+     */
+    public VersionResolutionException( VersionResult result )
+    {
+        super( getMessage( result ), getCause( result ) );
+        this.result = result;
+    }
+
+    private static String getMessage( VersionResult result )
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+        buffer.append( "Failed to resolve version" );
+        if ( result != null )
+        {
+            buffer.append( " for " ).append( result.getRequest().getArtifact() );
+            if ( !result.getExceptions().isEmpty() )
+            {
+                buffer.append( ": " ).append( result.getExceptions().iterator().next().getMessage() );
+            }
+        }
+        return buffer.toString();
+    }
+
+    private static Throwable getCause( VersionResult result )
+    {
+        Throwable cause = null;
+        if ( result != null && !result.getExceptions().isEmpty() )
+        {
+            cause = result.getExceptions().get( 0 );
+        }
+        return cause;
+    }
+
+    /**
+     * Creates a new exception with the specified result and detail message.
+     * 
+     * @param result The version result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public VersionResolutionException( VersionResult result, String message )
+    {
+        super( message, getCause( result ) );
+        this.result = result;
+    }
+
+    /**
+     * Creates a new exception with the specified result, detail message and cause.
+     * 
+     * @param result The version result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public VersionResolutionException( VersionResult result, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.result = result;
+    }
+
+    /**
+     * Gets the version result at the point the exception occurred. Despite being incomplete, callers might want to use
+     * this result to fail gracefully and continue their operation with whatever interim data has been gathered.
+     * 
+     * @return The version result or {@code null} if unknown.
+     */
+    public VersionResult getResult()
+    {
+        return result;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionResult.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/VersionResult.java
new file mode 100644 (file)
index 0000000..2e76b1c
--- /dev/null
@@ -0,0 +1,141 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.resolution;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.ArtifactRepository;
+
+/**
+ * The result of a version resolution request.
+ * 
+ * @see RepositorySystem#resolveVersion(RepositorySystemSession, VersionRequest)
+ */
+public final class VersionResult
+{
+
+    private final VersionRequest request;
+
+    private List<Exception> exceptions;
+
+    private String version;
+
+    private ArtifactRepository repository;
+
+    /**
+     * Creates a new result for the specified request.
+     * 
+     * @param request The resolution request, must not be {@code null}.
+     */
+    public VersionResult( VersionRequest request )
+    {
+        if ( request == null )
+        {
+            throw new IllegalArgumentException( "version request has not been specified" );
+        }
+        this.request = request;
+        exceptions = Collections.emptyList();
+    }
+
+    /**
+     * Gets the resolution request that was made.
+     * 
+     * @return The resolution request, never {@code null}.
+     */
+    public VersionRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the exceptions that occurred while resolving the version.
+     * 
+     * @return The exceptions that occurred, never {@code null}.
+     */
+    public List<Exception> getExceptions()
+    {
+        return exceptions;
+    }
+
+    /**
+     * Records the specified exception while resolving the version.
+     * 
+     * @param exception The exception to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public VersionResult addException( Exception exception )
+    {
+        if ( exception != null )
+        {
+            if ( exceptions.isEmpty() )
+            {
+                exceptions = new ArrayList<Exception>();
+            }
+            exceptions.add( exception );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the resolved version.
+     * 
+     * @return The resolved version or {@code null} if the resolution failed.
+     */
+    public String getVersion()
+    {
+        return version;
+    }
+
+    /**
+     * Sets the resolved version.
+     * 
+     * @param version The resolved version, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public VersionResult setVersion( String version )
+    {
+        this.version = version;
+        return this;
+    }
+
+    /**
+     * Gets the repository from which the version was eventually resolved.
+     * 
+     * @return The repository from which the version was resolved or {@code null} if unknown.
+     */
+    public ArtifactRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the repository from which the version was resolved.
+     * 
+     * @param repository The repository from which the version was resolved, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public VersionResult setRepository( ArtifactRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getVersion() + " @ " + getRepository();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/resolution/package-info.java b/org.argeo.slc.repo/src/org/eclipse/aether/resolution/package-info.java
new file mode 100644 (file)
index 0000000..84b825b
--- /dev/null
@@ -0,0 +1,15 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+/**
+ * The types supporting the resolution of artifacts and metadata from repositories.
+ */
+package org.eclipse.aether.resolution;
+
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/transfer/AbstractTransferListener.java b/org.argeo.slc.repo/src/org/eclipse/aether/transfer/AbstractTransferListener.java
new file mode 100644 (file)
index 0000000..01aff17
--- /dev/null
@@ -0,0 +1,55 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.transfer;
+
+/**
+ * A skeleton implementation for custom transfer listeners. The callback methods in this class do nothing.
+ */
+public abstract class AbstractTransferListener
+    implements TransferListener
+{
+
+    /**
+     * Enables subclassing.
+     */
+    protected AbstractTransferListener()
+    {
+    }
+
+    public void transferInitiated( TransferEvent event )
+        throws TransferCancelledException
+    {
+    }
+
+    public void transferStarted( TransferEvent event )
+        throws TransferCancelledException
+    {
+    }
+
+    public void transferProgressed( TransferEvent event )
+        throws TransferCancelledException
+    {
+    }
+
+    public void transferCorrupted( TransferEvent event )
+        throws TransferCancelledException
+    {
+    }
+
+    public void transferSucceeded( TransferEvent event )
+    {
+    }
+
+    public void transferFailed( TransferEvent event )
+    {
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/transfer/ArtifactNotFoundException.java b/org.argeo.slc.repo/src/org/eclipse/aether/transfer/ArtifactNotFoundException.java
new file mode 100644 (file)
index 0000000..3813743
--- /dev/null
@@ -0,0 +1,95 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.transfer;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.ArtifactProperties;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Thrown when an artifact was not found in a particular repository.
+ */
+public class ArtifactNotFoundException
+    extends ArtifactTransferException
+{
+
+    /**
+     * Creates a new exception with the specified artifact and repository.
+     * 
+     * @param artifact The missing artifact, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     */
+    public ArtifactNotFoundException( Artifact artifact, RemoteRepository repository )
+    {
+        super( artifact, repository, getMessage( artifact, repository ) );
+    }
+
+    private static String getMessage( Artifact artifact, RemoteRepository repository )
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+        buffer.append( "Could not find artifact " ).append( artifact );
+        buffer.append( getString( " in ", repository ) );
+        if ( artifact != null )
+        {
+            String localPath = artifact.getProperty( ArtifactProperties.LOCAL_PATH, null );
+            if ( localPath != null && repository == null )
+            {
+                buffer.append( " at specified path " ).append( localPath );
+            }
+            String downloadUrl = artifact.getProperty( ArtifactProperties.DOWNLOAD_URL, null );
+            if ( downloadUrl != null )
+            {
+                buffer.append( ", try downloading from " ).append( downloadUrl );
+            }
+        }
+        return buffer.toString();
+    }
+
+    /**
+     * Creates a new exception with the specified artifact, repository and detail message.
+     * 
+     * @param artifact The missing artifact, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public ArtifactNotFoundException( Artifact artifact, RemoteRepository repository, String message )
+    {
+        super( artifact, repository, message );
+    }
+
+    /**
+     * Creates a new exception with the specified artifact, repository and detail message.
+     * 
+     * @param artifact The missing artifact, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param fromCache {@code true} if the exception was played back from the error cache, {@code false} if the
+     *            exception actually just occurred.
+     */
+    public ArtifactNotFoundException( Artifact artifact, RemoteRepository repository, String message, boolean fromCache )
+    {
+        super( artifact, repository, message, fromCache );
+    }
+
+    /**
+     * Creates a new exception with the specified artifact, repository, detail message and cause.
+     * 
+     * @param artifact The missing artifact, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public ArtifactNotFoundException( Artifact artifact, RemoteRepository repository, String message, Throwable cause )
+    {
+        super( artifact, repository, message, cause );
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/transfer/ArtifactTransferException.java b/org.argeo.slc.repo/src/org/eclipse/aether/transfer/ArtifactTransferException.java
new file mode 100644 (file)
index 0000000..5a481ee
--- /dev/null
@@ -0,0 +1,131 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.transfer;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Thrown when an artifact could not be uploaded/downloaded to/from a particular remote repository.
+ */
+public class ArtifactTransferException
+    extends RepositoryException
+{
+
+    private final transient Artifact artifact;
+
+    private final transient RemoteRepository repository;
+
+    private final boolean fromCache;
+
+    static String getString( String prefix, RemoteRepository repository )
+    {
+        if ( repository == null )
+        {
+            return "";
+        }
+        else
+        {
+            return prefix + repository.getId() + " (" + repository.getUrl() + ")";
+        }
+    }
+
+    /**
+     * Creates a new exception with the specified artifact, repository and detail message.
+     * 
+     * @param artifact The untransferable artifact, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public ArtifactTransferException( Artifact artifact, RemoteRepository repository, String message )
+    {
+        this( artifact, repository, message, false );
+    }
+
+    /**
+     * Creates a new exception with the specified artifact, repository and detail message.
+     * 
+     * @param artifact The untransferable artifact, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param fromCache {@code true} if the exception was played back from the error cache, {@code false} if the
+     *            exception actually just occurred.
+     */
+    public ArtifactTransferException( Artifact artifact, RemoteRepository repository, String message, boolean fromCache )
+    {
+        super( message );
+        this.artifact = artifact;
+        this.repository = repository;
+        this.fromCache = fromCache;
+    }
+
+    /**
+     * Creates a new exception with the specified artifact, repository and cause.
+     * 
+     * @param artifact The untransferable artifact, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public ArtifactTransferException( Artifact artifact, RemoteRepository repository, Throwable cause )
+    {
+        this( artifact, repository, "Could not transfer artifact " + artifact + getString( " from/to ", repository )
+            + getMessage( ": ", cause ), cause );
+    }
+
+    /**
+     * Creates a new exception with the specified artifact, repository, detail message and cause.
+     * 
+     * @param artifact The untransferable artifact, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public ArtifactTransferException( Artifact artifact, RemoteRepository repository, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.artifact = artifact;
+        this.repository = repository;
+        this.fromCache = false;
+    }
+
+    /**
+     * Gets the artifact that could not be transferred.
+     * 
+     * @return The troublesome artifact or {@code null} if unknown.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Gets the remote repository involved in the transfer.
+     * 
+     * @return The involved remote repository or {@code null} if unknown.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Indicates whether this exception actually just occurred or was played back from the error cache.
+     * 
+     * @return {@code true} if the exception was played back from the error cache, {@code false} if the exception
+     *         actually occurred just now.
+     */
+    public boolean isFromCache()
+    {
+        return fromCache;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/transfer/ChecksumFailureException.java b/org.argeo.slc.repo/src/org/eclipse/aether/transfer/ChecksumFailureException.java
new file mode 100644 (file)
index 0000000..e3f248a
--- /dev/null
@@ -0,0 +1,122 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.transfer;
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of a checksum failure during an artifact/metadata download.
+ */
+public class ChecksumFailureException
+    extends RepositoryException
+{
+
+    private final String expected;
+
+    private final String actual;
+
+    private final boolean retryWorthy;
+
+    /**
+     * Creates a new exception with the specified expected and actual checksum. The resulting exception is
+     * {@link #isRetryWorthy() retry-worthy}.
+     * 
+     * @param expected The expected checksum as declared by the hosting repository, may be {@code null}.
+     * @param actual The actual checksum as computed from the local bytes, may be {@code null}.
+     */
+    public ChecksumFailureException( String expected, String actual )
+    {
+        super( "Checksum validation failed, expected " + expected + " but is " + actual );
+        this.expected = expected;
+        this.actual = actual;
+        retryWorthy = true;
+    }
+
+    /**
+     * Creates a new exception with the specified detail message. The resulting exception is not
+     * {@link #isRetryWorthy() retry-worthy}.
+     * 
+     * @param message The detail message, may be {@code null}.
+     */
+    public ChecksumFailureException( String message )
+    {
+        this( false, message, null );
+    }
+
+    /**
+     * Creates a new exception with the specified cause. The resulting exception is not {@link #isRetryWorthy()
+     * retry-worthy}.
+     * 
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public ChecksumFailureException( Throwable cause )
+    {
+        this( "Checksum validation failed" + getMessage( ": ", cause ), cause );
+    }
+
+    /**
+     * Creates a new exception with the specified detail message and cause. The resulting exception is not
+     * {@link #isRetryWorthy() retry-worthy}.
+     * 
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public ChecksumFailureException( String message, Throwable cause )
+    {
+        this( false, message, cause );
+    }
+
+    /**
+     * Creates a new exception with the specified retry flag, detail message and cause.
+     * 
+     * @param retryWorthy {@code true} if the exception is retry-worthy, {@code false} otherwise.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public ChecksumFailureException( boolean retryWorthy, String message, Throwable cause )
+    {
+        super( message, cause );
+        expected = actual = "";
+        this.retryWorthy = retryWorthy;
+    }
+
+    /**
+     * Gets the expected checksum for the downloaded artifact/metadata.
+     * 
+     * @return The expected checksum as declared by the hosting repository or {@code null} if unknown.
+     */
+    public String getExpected()
+    {
+        return expected;
+    }
+
+    /**
+     * Gets the actual checksum for the downloaded artifact/metadata.
+     * 
+     * @return The actual checksum as computed from the local bytes or {@code null} if unknown.
+     */
+    public String getActual()
+    {
+        return actual;
+    }
+
+    /**
+     * Indicates whether the corresponding download is retry-worthy.
+     * 
+     * @return {@code true} if retrying the download might solve the checksum failure, {@code false} if the checksum
+     *         failure is non-recoverable.
+     */
+    public boolean isRetryWorthy()
+    {
+        return retryWorthy;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/transfer/MetadataNotFoundException.java b/org.argeo.slc.repo/src/org/eclipse/aether/transfer/MetadataNotFoundException.java
new file mode 100644 (file)
index 0000000..af9a840
--- /dev/null
@@ -0,0 +1,97 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.transfer;
+
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Thrown when metadata was not found in a particular repository.
+ */
+public class MetadataNotFoundException
+    extends MetadataTransferException
+{
+
+    /**
+     * Creates a new exception with the specified metadata and local repository.
+     * 
+     * @param metadata The missing metadata, may be {@code null}.
+     * @param repository The involved local repository, may be {@code null}.
+     */
+    public MetadataNotFoundException( Metadata metadata, LocalRepository repository )
+    {
+        super( metadata, null, "Could not find metadata " + metadata + getString( " in ", repository ) );
+    }
+
+    private static String getString( String prefix, LocalRepository repository )
+    {
+        if ( repository == null )
+        {
+            return "";
+        }
+        else
+        {
+            return prefix + repository.getId() + " (" + repository.getBasedir() + ")";
+        }
+    }
+
+    /**
+     * Creates a new exception with the specified metadata and repository.
+     * 
+     * @param metadata The missing metadata, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     */
+    public MetadataNotFoundException( Metadata metadata, RemoteRepository repository )
+    {
+        super( metadata, repository, "Could not find metadata " + metadata + getString( " in ", repository ) );
+    }
+
+    /**
+     * Creates a new exception with the specified metadata, repository and detail message.
+     * 
+     * @param metadata The missing metadata, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public MetadataNotFoundException( Metadata metadata, RemoteRepository repository, String message )
+    {
+        super( metadata, repository, message );
+    }
+
+    /**
+     * Creates a new exception with the specified metadata, repository and detail message.
+     * 
+     * @param metadata The missing metadata, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param fromCache {@code true} if the exception was played back from the error cache, {@code false} if the
+     *            exception actually just occurred.
+     */
+    public MetadataNotFoundException( Metadata metadata, RemoteRepository repository, String message, boolean fromCache )
+    {
+        super( metadata, repository, message, fromCache );
+    }
+
+    /**
+     * Creates a new exception with the specified metadata, repository, detail message and cause.
+     * 
+     * @param metadata The missing metadata, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public MetadataNotFoundException( Metadata metadata, RemoteRepository repository, String message, Throwable cause )
+    {
+        super( metadata, repository, message, cause );
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/transfer/MetadataTransferException.java b/org.argeo.slc.repo/src/org/eclipse/aether/transfer/MetadataTransferException.java
new file mode 100644 (file)
index 0000000..f86b986
--- /dev/null
@@ -0,0 +1,131 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.transfer;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Thrown when metadata could not be uploaded/downloaded to/from a particular remote repository.
+ */
+public class MetadataTransferException
+    extends RepositoryException
+{
+
+    private final transient Metadata metadata;
+
+    private final transient RemoteRepository repository;
+
+    private final boolean fromCache;
+
+    static String getString( String prefix, RemoteRepository repository )
+    {
+        if ( repository == null )
+        {
+            return "";
+        }
+        else
+        {
+            return prefix + repository.getId() + " (" + repository.getUrl() + ")";
+        }
+    }
+
+    /**
+     * Creates a new exception with the specified metadata, repository and detail message.
+     * 
+     * @param metadata The untransferable metadata, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public MetadataTransferException( Metadata metadata, RemoteRepository repository, String message )
+    {
+        this( metadata, repository, message, false );
+    }
+
+    /**
+     * Creates a new exception with the specified metadata, repository and detail message.
+     * 
+     * @param metadata The untransferable metadata, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param fromCache {@code true} if the exception was played back from the error cache, {@code false} if the
+     *            exception actually just occurred.
+     */
+    public MetadataTransferException( Metadata metadata, RemoteRepository repository, String message, boolean fromCache )
+    {
+        super( message );
+        this.metadata = metadata;
+        this.repository = repository;
+        this.fromCache = fromCache;
+    }
+
+    /**
+     * Creates a new exception with the specified metadata, repository and cause.
+     * 
+     * @param metadata The untransferable metadata, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public MetadataTransferException( Metadata metadata, RemoteRepository repository, Throwable cause )
+    {
+        this( metadata, repository, "Could not transfer metadata " + metadata + getString( " from/to ", repository )
+            + getMessage( ": ", cause ), cause );
+    }
+
+    /**
+     * Creates a new exception with the specified metadata, repository, detail message and cause.
+     * 
+     * @param metadata The untransferable metadata, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public MetadataTransferException( Metadata metadata, RemoteRepository repository, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.metadata = metadata;
+        this.repository = repository;
+        this.fromCache = false;
+    }
+
+    /**
+     * Gets the metadata that could not be transferred.
+     * 
+     * @return The troublesome metadata or {@code null} if unknown.
+     */
+    public Metadata getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Gets the remote repository involved in the transfer.
+     * 
+     * @return The involved remote repository or {@code null} if unknown.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Indicates whether this exception actually just occurred or was played back from the error cache.
+     * 
+     * @return {@code true} if the exception was played back from the error cache, {@code false} if the exception
+     *         actually occurred just now.
+     */
+    public boolean isFromCache()
+    {
+        return fromCache;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/transfer/NoRepositoryConnectorException.java b/org.argeo.slc.repo/src/org/eclipse/aether/transfer/NoRepositoryConnectorException.java
new file mode 100644 (file)
index 0000000..c91be2b
--- /dev/null
@@ -0,0 +1,94 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.transfer;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Thrown in case of an unsupported remote repository type.
+ */
+public class NoRepositoryConnectorException
+    extends RepositoryException
+{
+
+    private final transient RemoteRepository repository;
+
+    /**
+     * Creates a new exception with the specified repository.
+     * 
+     * @param repository The remote repository whose content type is not supported, may be {@code null}.
+     */
+    public NoRepositoryConnectorException( RemoteRepository repository )
+    {
+        this( repository, toMessage( repository ) );
+    }
+
+    /**
+     * Creates a new exception with the specified repository and detail message.
+     * 
+     * @param repository The remote repository whose content type is not supported, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public NoRepositoryConnectorException( RemoteRepository repository, String message )
+    {
+        super( message );
+        this.repository = repository;
+    }
+
+    /**
+     * Creates a new exception with the specified repository and cause.
+     * 
+     * @param repository The remote repository whose content type is not supported, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public NoRepositoryConnectorException( RemoteRepository repository, Throwable cause )
+    {
+        this( repository, toMessage( repository ), cause );
+    }
+
+    /**
+     * Creates a new exception with the specified repository, detail message and cause.
+     * 
+     * @param repository The remote repository whose content type is not supported, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public NoRepositoryConnectorException( RemoteRepository repository, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.repository = repository;
+    }
+
+    private static String toMessage( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            return "No connector available to access repository " + repository.getId() + " (" + repository.getUrl()
+                + ") of type " + repository.getContentType();
+        }
+        else
+        {
+            return "No connector available to access repository";
+        }
+    }
+
+    /**
+     * Gets the remote repository whose content type is not supported.
+     * 
+     * @return The unsupported remote repository or {@code null} if unknown.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/transfer/NoRepositoryLayoutException.java b/org.argeo.slc.repo/src/org/eclipse/aether/transfer/NoRepositoryLayoutException.java
new file mode 100644 (file)
index 0000000..3176601
--- /dev/null
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * Copyright (c) 2013, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.transfer;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Thrown in case of an unsupported repository layout.
+ */
+public class NoRepositoryLayoutException
+    extends RepositoryException
+{
+
+    private final transient RemoteRepository repository;
+
+    /**
+     * Creates a new exception with the specified repository.
+     * 
+     * @param repository The remote repository whose layout is not supported, may be {@code null}.
+     */
+    public NoRepositoryLayoutException( RemoteRepository repository )
+    {
+        this( repository, toMessage( repository ) );
+    }
+
+    /**
+     * Creates a new exception with the specified repository and detail message.
+     * 
+     * @param repository The remote repository whose layout is not supported, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public NoRepositoryLayoutException( RemoteRepository repository, String message )
+    {
+        super( message );
+        this.repository = repository;
+    }
+
+    /**
+     * Creates a new exception with the specified repository and cause.
+     * 
+     * @param repository The remote repository whose layout is not supported, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public NoRepositoryLayoutException( RemoteRepository repository, Throwable cause )
+    {
+        this( repository, toMessage( repository ), cause );
+    }
+
+    /**
+     * Creates a new exception with the specified repository, detail message and cause.
+     * 
+     * @param repository The remote repository whose layout is not supported, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public NoRepositoryLayoutException( RemoteRepository repository, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.repository = repository;
+    }
+
+    private static String toMessage( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            return "Unsupported repository layout " + repository.getContentType();
+        }
+        else
+        {
+            return "Unsupported repository layout";
+        }
+    }
+
+    /**
+     * Gets the remote repository whose layout is not supported.
+     * 
+     * @return The unsupported remote repository or {@code null} if unknown.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/transfer/NoTransporterException.java b/org.argeo.slc.repo/src/org/eclipse/aether/transfer/NoTransporterException.java
new file mode 100644 (file)
index 0000000..895b066
--- /dev/null
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * Copyright (c) 2013, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.transfer;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Thrown in case of an unsupported transport protocol.
+ */
+public class NoTransporterException
+    extends RepositoryException
+{
+
+    private final transient RemoteRepository repository;
+
+    /**
+     * Creates a new exception with the specified repository.
+     * 
+     * @param repository The remote repository whose transport layout is not supported, may be {@code null}.
+     */
+    public NoTransporterException( RemoteRepository repository )
+    {
+        this( repository, toMessage( repository ) );
+    }
+
+    /**
+     * Creates a new exception with the specified repository and detail message.
+     * 
+     * @param repository The remote repository whose transport layout is not supported, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public NoTransporterException( RemoteRepository repository, String message )
+    {
+        super( message );
+        this.repository = repository;
+    }
+
+    /**
+     * Creates a new exception with the specified repository and cause.
+     * 
+     * @param repository The remote repository whose transport layout is not supported, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public NoTransporterException( RemoteRepository repository, Throwable cause )
+    {
+        this( repository, toMessage( repository ), cause );
+    }
+
+    /**
+     * Creates a new exception with the specified repository, detail message and cause.
+     * 
+     * @param repository The remote repository whose transport layout is not supported, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public NoTransporterException( RemoteRepository repository, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.repository = repository;
+    }
+
+    private static String toMessage( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            return "Unsupported transport protocol " + repository.getProtocol();
+        }
+        else
+        {
+            return "Unsupported transport protocol";
+        }
+    }
+
+    /**
+     * Gets the remote repository whose transport protocol is not supported.
+     * 
+     * @return The unsupported remote repository or {@code null} if unknown.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/transfer/RepositoryOfflineException.java b/org.argeo.slc.repo/src/org/eclipse/aether/transfer/RepositoryOfflineException.java
new file mode 100644 (file)
index 0000000..1115054
--- /dev/null
@@ -0,0 +1,70 @@
+/*******************************************************************************
+ * Copyright (c) 2012, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.transfer;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Thrown when a transfer could not be performed because a remote repository is not accessible in offline mode.
+ */
+public class RepositoryOfflineException
+    extends RepositoryException
+{
+
+    private final transient RemoteRepository repository;
+
+    private static String getMessage( RemoteRepository repository )
+    {
+        if ( repository == null )
+        {
+            return "Cannot access remote repositories in offline mode";
+        }
+        else
+        {
+            return "Cannot access " + repository.getId() + " (" + repository.getUrl() + ") in offline mode";
+        }
+    }
+
+    /**
+     * Creates a new exception with the specified repository.
+     * 
+     * @param repository The inaccessible remote repository, may be {@code null}.
+     */
+    public RepositoryOfflineException( RemoteRepository repository )
+    {
+        super( getMessage( repository ) );
+        this.repository = repository;
+    }
+
+    /**
+     * Creates a new exception with the specified repository and detail message.
+     * 
+     * @param repository The inaccessible remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public RepositoryOfflineException( RemoteRepository repository, String message )
+    {
+        super( message );
+        this.repository = repository;
+    }
+
+    /**
+     * Gets the remote repository that could not be accessed due to offline mode.
+     * 
+     * @return The inaccessible remote repository or {@code null} if unknown.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/transfer/TransferCancelledException.java b/org.argeo.slc.repo/src/org/eclipse/aether/transfer/TransferCancelledException.java
new file mode 100644 (file)
index 0000000..5f4ed5b
--- /dev/null
@@ -0,0 +1,51 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.transfer;
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case an upload/download was cancelled (e.g. due to user request).
+ */
+public class TransferCancelledException
+    extends RepositoryException
+{
+
+    /**
+     * Creates a new exception with a stock detail message.
+     */
+    public TransferCancelledException()
+    {
+        super( "The operation was cancelled." );
+    }
+
+    /**
+     * Creates a new exception with the specified detail message.
+     * 
+     * @param message The detail message, may be {@code null}.
+     */
+    public TransferCancelledException( String message )
+    {
+        super( message );
+    }
+
+    /**
+     * Creates a new exception with the specified detail message and cause.
+     * 
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public TransferCancelledException( String message, Throwable cause )
+    {
+        super( message, cause );
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/transfer/TransferEvent.java b/org.argeo.slc.repo/src/org/eclipse/aether/transfer/TransferEvent.java
new file mode 100644 (file)
index 0000000..9be298f
--- /dev/null
@@ -0,0 +1,423 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.transfer;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * An event fired to a transfer listener during an artifact/metadata transfer.
+ * 
+ * @see TransferListener
+ * @see TransferEvent.Builder
+ */
+public final class TransferEvent
+{
+
+    /**
+     * The type of the event.
+     */
+    public enum EventType
+    {
+
+        /**
+         * @see TransferListener#transferInitiated(TransferEvent)
+         */
+        INITIATED,
+
+        /**
+         * @see TransferListener#transferStarted(TransferEvent)
+         */
+        STARTED,
+
+        /**
+         * @see TransferListener#transferProgressed(TransferEvent)
+         */
+        PROGRESSED,
+
+        /**
+         * @see TransferListener#transferCorrupted(TransferEvent)
+         */
+        CORRUPTED,
+
+        /**
+         * @see TransferListener#transferSucceeded(TransferEvent)
+         */
+        SUCCEEDED,
+
+        /**
+         * @see TransferListener#transferFailed(TransferEvent)
+         */
+        FAILED
+
+    }
+
+    /**
+     * The type of the request/transfer being performed.
+     */
+    public enum RequestType
+    {
+
+        /**
+         * Download artifact/metadata.
+         */
+        GET,
+
+        /**
+         * Check artifact/metadata existence only.
+         */
+        GET_EXISTENCE,
+
+        /**
+         * Upload artifact/metadata.
+         */
+        PUT,
+
+    }
+
+    private final EventType type;
+
+    private final RequestType requestType;
+
+    private final RepositorySystemSession session;
+
+    private final TransferResource resource;
+
+    private final ByteBuffer dataBuffer;
+
+    private final long transferredBytes;
+
+    private final Exception exception;
+
+    TransferEvent( Builder builder )
+    {
+        type = builder.type;
+        requestType = builder.requestType;
+        session = builder.session;
+        resource = builder.resource;
+        dataBuffer = builder.dataBuffer;
+        transferredBytes = builder.transferredBytes;
+        exception = builder.exception;
+    }
+
+    /**
+     * Gets the type of the event.
+     * 
+     * @return The type of the event, never {@code null}.
+     */
+    public EventType getType()
+    {
+        return type;
+    }
+
+    /**
+     * Gets the type of the request/transfer.
+     * 
+     * @return The type of the request/transfer, never {@code null}.
+     */
+    public RequestType getRequestType()
+    {
+        return requestType;
+    }
+
+    /**
+     * Gets the repository system session during which the event occurred.
+     * 
+     * @return The repository system session during which the event occurred, never {@code null}.
+     */
+    public RepositorySystemSession getSession()
+    {
+        return session;
+    }
+
+    /**
+     * Gets the resource that is being transferred.
+     * 
+     * @return The resource being transferred, never {@code null}.
+     */
+    public TransferResource getResource()
+    {
+        return resource;
+    }
+
+    /**
+     * Gets the total number of bytes that have been transferred since the download/upload of the resource was started.
+     * If a download has been resumed, the returned count includes the bytes that were already downloaded during the
+     * previous attempt. In other words, the ratio of transferred bytes to the content length of the resource indicates
+     * the percentage of transfer completion.
+     * 
+     * @return The total number of bytes that have been transferred since the transfer started, never negative.
+     * @see #getDataLength()
+     * @see TransferResource#getResumeOffset()
+     */
+    public long getTransferredBytes()
+    {
+        return transferredBytes;
+    }
+
+    /**
+     * Gets the byte buffer holding the transferred bytes since the last event. A listener must assume this buffer to be
+     * owned by the event source and must not change any byte in this buffer. Also, the buffer is only valid for the
+     * duration of the event callback, i.e. the next event might reuse the same buffer (with updated contents).
+     * Therefore, if the actual event processing is deferred, the byte buffer would have to be cloned to create an
+     * immutable snapshot of its contents.
+     * 
+     * @return The (read-only) byte buffer or {@code null} if not applicable to the event, i.e. if the event type is not
+     *         {@link EventType#PROGRESSED}.
+     */
+    public ByteBuffer getDataBuffer()
+    {
+        return ( dataBuffer != null ) ? dataBuffer.asReadOnlyBuffer() : null;
+    }
+
+    /**
+     * Gets the number of bytes that have been transferred since the last event.
+     * 
+     * @return The number of bytes that have been transferred since the last event, possibly zero but never negative.
+     * @see #getTransferredBytes()
+     */
+    public int getDataLength()
+    {
+        return ( dataBuffer != null ) ? dataBuffer.remaining() : 0;
+    }
+
+    /**
+     * Gets the error that occurred during the transfer.
+     * 
+     * @return The error that occurred or {@code null} if none.
+     */
+    public Exception getException()
+    {
+        return exception;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getRequestType() + " " + getType() + " " + getResource();
+    }
+
+    /**
+     * A builder to create transfer events.
+     */
+    public static final class Builder
+    {
+
+        EventType type;
+
+        RequestType requestType;
+
+        RepositorySystemSession session;
+
+        TransferResource resource;
+
+        ByteBuffer dataBuffer;
+
+        long transferredBytes;
+
+        Exception exception;
+
+        /**
+         * Creates a new transfer event builder for the specified session and the given resource.
+         * 
+         * @param session The repository system session, must not be {@code null}.
+         * @param resource The resource being transferred, must not be {@code null}.
+         */
+        public Builder( RepositorySystemSession session, TransferResource resource )
+        {
+            if ( session == null )
+            {
+                throw new IllegalArgumentException( "session not specified" );
+            }
+            if ( resource == null )
+            {
+                throw new IllegalArgumentException( "transfer resource not specified" );
+            }
+            this.session = session;
+            this.resource = resource;
+            type = EventType.INITIATED;
+            requestType = RequestType.GET;
+        }
+
+        private Builder( Builder prototype )
+        {
+            session = prototype.session;
+            resource = prototype.resource;
+            type = prototype.type;
+            requestType = prototype.requestType;
+            dataBuffer = prototype.dataBuffer;
+            transferredBytes = prototype.transferredBytes;
+            exception = prototype.exception;
+        }
+
+        /**
+         * Creates a new transfer event builder from the current values of this builder. The state of this builder
+         * remains unchanged.
+         * 
+         * @return The new event builder, never {@code null}.
+         */
+        public Builder copy()
+        {
+            return new Builder( this );
+        }
+
+        /**
+         * Sets the type of the event and resets event-specific fields. In more detail, the data buffer and the
+         * exception fields are set to {@code null}. Furthermore, the total number of transferred bytes is set to
+         * {@code 0} if the event type is {@link EventType#STARTED}.
+         * 
+         * @param type The type of the event, must not be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder resetType( EventType type )
+        {
+            if ( type == null )
+            {
+                throw new IllegalArgumentException( "event type not specified" );
+            }
+            this.type = type;
+            dataBuffer = null;
+            exception = null;
+            switch ( type )
+            {
+                case INITIATED:
+                case STARTED:
+                    transferredBytes = 0;
+                default:
+            }
+            return this;
+        }
+
+        /**
+         * Sets the type of the event. When re-using the same builder to generate a sequence of events for one transfer,
+         * {@link #resetType(TransferEvent.EventType)} might be more handy.
+         * 
+         * @param type The type of the event, must not be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setType( EventType type )
+        {
+            if ( type == null )
+            {
+                throw new IllegalArgumentException( "event type not specified" );
+            }
+            this.type = type;
+            return this;
+        }
+
+        /**
+         * Sets the type of the request/transfer.
+         * 
+         * @param requestType The request/transfer type, must not be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setRequestType( RequestType requestType )
+        {
+            if ( requestType == null )
+            {
+                throw new IllegalArgumentException( "request type not specified" );
+            }
+            this.requestType = requestType;
+            return this;
+        }
+
+        /**
+         * Sets the total number of bytes that have been transferred so far during the download/upload of the resource.
+         * If a download is being resumed, the count must include the bytes that were already downloaded in the previous
+         * attempt and from which the current transfer started. In this case, the event type {@link EventType#STARTED}
+         * should indicate from what byte the download resumes.
+         * 
+         * @param transferredBytes The total number of bytes that have been transferred so far during the
+         *            download/upload of the resource, must not be negative.
+         * @return This event builder for chaining, never {@code null}.
+         * @see TransferResource#setResumeOffset(long)
+         */
+        public Builder setTransferredBytes( long transferredBytes )
+        {
+            if ( transferredBytes < 0 )
+            {
+                throw new IllegalArgumentException( "number of transferred bytes cannot be negative" );
+            }
+            this.transferredBytes = transferredBytes;
+            return this;
+        }
+
+        /**
+         * Increments the total number of bytes that have been transferred so far during the download/upload.
+         * 
+         * @param transferredBytes The number of bytes that have been transferred since the last event, must not be
+         *            negative.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder addTransferredBytes( long transferredBytes )
+        {
+            if ( transferredBytes < 0 )
+            {
+                throw new IllegalArgumentException( "number of transferred bytes cannot be negative" );
+            }
+            this.transferredBytes += transferredBytes;
+            return this;
+        }
+
+        /**
+         * Sets the byte buffer holding the transferred bytes since the last event.
+         * 
+         * @param buffer The byte buffer holding the transferred bytes since the last event, may be {@code null} if not
+         *            applicable to the event.
+         * @param offset The starting point of valid bytes in the array.
+         * @param length The number of valid bytes, must not be negative.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setDataBuffer( byte[] buffer, int offset, int length )
+        {
+            return setDataBuffer( ( buffer != null ) ? ByteBuffer.wrap( buffer, offset, length ) : null );
+        }
+
+        /**
+         * Sets the byte buffer holding the transferred bytes since the last event.
+         * 
+         * @param dataBuffer The byte buffer holding the transferred bytes since the last event, may be {@code null} if
+         *            not applicable to the event.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setDataBuffer( ByteBuffer dataBuffer )
+        {
+            this.dataBuffer = dataBuffer;
+            return this;
+        }
+
+        /**
+         * Sets the error that occurred during the transfer.
+         * 
+         * @param exception The error that occurred during the transfer, may be {@code null} if none.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setException( Exception exception )
+        {
+            this.exception = exception;
+            return this;
+        }
+
+        /**
+         * Builds a new transfer event from the current values of this builder. The state of the builder itself remains
+         * unchanged.
+         * 
+         * @return The transfer event, never {@code null}.
+         */
+        public TransferEvent build()
+        {
+            return new TransferEvent( this );
+        }
+
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/transfer/TransferListener.java b/org.argeo.slc.repo/src/org/eclipse/aether/transfer/TransferListener.java
new file mode 100644 (file)
index 0000000..26c016d
--- /dev/null
@@ -0,0 +1,91 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.transfer;
+
+/**
+ * A listener being notified of artifact/metadata transfers from/to remote repositories. The listener may be called from
+ * an arbitrary thread. Reusing common regular expression syntax, the sequence of events is roughly as follows:
+ * 
+ * <pre>
+ * INITIATED ( STARTED PROGRESSED* CORRUPTED? )* ( SUCCEEDED | FAILED )
+ * </pre>
+ * 
+ * <em>Note:</em> Implementors are strongly advised to inherit from {@link AbstractTransferListener} instead of directly
+ * implementing this interface.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getTransferListener()
+ * @see org.eclipse.aether.RepositoryListener
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface TransferListener
+{
+
+    /**
+     * Notifies the listener about the initiation of a transfer. This event gets fired before any actual network access
+     * to the remote repository and usually indicates some thread is now about to perform the transfer. For a given
+     * transfer request, this event is the first one being fired and it must be emitted exactly once.
+     * 
+     * @param event The event details, must not be {@code null}.
+     * @throws TransferCancelledException If the transfer should be aborted.
+     */
+    void transferInitiated( TransferEvent event )
+        throws TransferCancelledException;
+
+    /**
+     * Notifies the listener about the start of a data transfer. This event indicates a successful connection to the
+     * remote repository. In case of a download, the requested remote resource exists and its size is given by
+     * {@link TransferResource#getContentLength()} if possible. This event may be fired multiple times for given
+     * transfer request if said transfer needs to be repeated (e.g. in response to an authentication challenge).
+     * 
+     * @param event The event details, must not be {@code null}.
+     * @throws TransferCancelledException If the transfer should be aborted.
+     */
+    void transferStarted( TransferEvent event )
+        throws TransferCancelledException;
+
+    /**
+     * Notifies the listener about some progress in the data transfer. This event may even be fired if actually zero
+     * bytes have been transferred since the last event, for instance to enable cancellation.
+     * 
+     * @param event The event details, must not be {@code null}.
+     * @throws TransferCancelledException If the transfer should be aborted.
+     */
+    void transferProgressed( TransferEvent event )
+        throws TransferCancelledException;
+
+    /**
+     * Notifies the listener that a checksum validation failed. {@link TransferEvent#getException()} will be of type
+     * {@link ChecksumFailureException} and can be used to query further details about the expected/actual checksums.
+     * 
+     * @param event The event details, must not be {@code null}.
+     * @throws TransferCancelledException If the transfer should be aborted.
+     */
+    void transferCorrupted( TransferEvent event )
+        throws TransferCancelledException;
+
+    /**
+     * Notifies the listener about the successful completion of a transfer. This event must be fired exactly once for a
+     * given transfer request unless said request failed.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void transferSucceeded( TransferEvent event );
+
+    /**
+     * Notifies the listener about the unsuccessful termination of a transfer. {@link TransferEvent#getException()} will
+     * provide further information about the failure.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void transferFailed( TransferEvent event );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/transfer/TransferResource.java b/org.argeo.slc.repo/src/org/eclipse/aether/transfer/TransferResource.java
new file mode 100644 (file)
index 0000000..b9510fb
--- /dev/null
@@ -0,0 +1,192 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2013 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.transfer;
+
+import java.io.File;
+
+import org.eclipse.aether.RequestTrace;
+
+/**
+ * Describes a resource being uploaded or downloaded by the repository system.
+ */
+public final class TransferResource
+{
+
+    private final String repositoryUrl;
+
+    private final String resourceName;
+
+    private final File file;
+
+    private final long startTime;
+
+    private final RequestTrace trace;
+
+    private long contentLength = -1;
+
+    private long resumeOffset;
+
+    /**
+     * Creates a new transfer resource with the specified properties.
+     * 
+     * @param repositoryUrl The base URL of the repository, may be {@code null} or empty if unknown. If not empty, a
+     *            trailing slash will automatically be added if missing.
+     * @param resourceName The relative path to the resource within the repository, may be {@code null}. A leading slash
+     *            (if any) will be automatically removed.
+     * @param file The source/target file involved in the transfer, may be {@code null}.
+     * @param trace The trace information, may be {@code null}.
+     */
+    public TransferResource( String repositoryUrl, String resourceName, File file, RequestTrace trace )
+    {
+        if ( repositoryUrl == null || repositoryUrl.length() <= 0 )
+        {
+            this.repositoryUrl = "";
+        }
+        else if ( repositoryUrl.endsWith( "/" ) )
+        {
+            this.repositoryUrl = repositoryUrl;
+        }
+        else
+        {
+            this.repositoryUrl = repositoryUrl + '/';
+        }
+
+        if ( resourceName == null || resourceName.length() <= 0 )
+        {
+            this.resourceName = "";
+        }
+        else if ( resourceName.startsWith( "/" ) )
+        {
+            this.resourceName = resourceName.substring( 1 );
+        }
+        else
+        {
+            this.resourceName = resourceName;
+        }
+
+        this.file = file;
+
+        this.trace = trace;
+
+        startTime = System.currentTimeMillis();
+    }
+
+    /**
+     * The base URL of the repository, e.g. "http://repo1.maven.org/maven2/". Unless the URL is unknown, it will be
+     * terminated by a trailing slash.
+     * 
+     * @return The base URL of the repository or an empty string if unknown, never {@code null}.
+     */
+    public String getRepositoryUrl()
+    {
+        return repositoryUrl;
+    }
+
+    /**
+     * The path of the resource relative to the repository's base URL, e.g. "org/apache/maven/maven/3.0/maven-3.0.pom".
+     * 
+     * @return The path of the resource, never {@code null}.
+     */
+    public String getResourceName()
+    {
+        return resourceName;
+    }
+
+    /**
+     * Gets the local file being uploaded or downloaded. When the repository system merely checks for the existence of a
+     * remote resource, no local file will be involved in the transfer.
+     * 
+     * @return The source/target file involved in the transfer or {@code null} if none.
+     */
+    public File getFile()
+    {
+        return file;
+    }
+
+    /**
+     * The size of the resource in bytes. Note that the size of a resource during downloads might be unknown to the
+     * client which is usually the case when transfers employ compression like gzip. In general, the content length is
+     * not known until the transfer has {@link TransferListener#transferStarted(TransferEvent) started}.
+     * 
+     * @return The size of the resource in bytes or a negative value if unknown.
+     */
+    public long getContentLength()
+    {
+        return contentLength;
+    }
+
+    /**
+     * Sets the size of the resource in bytes.
+     * 
+     * @param contentLength The size of the resource in bytes or a negative value if unknown.
+     * @return This resource for chaining, never {@code null}.
+     */
+    public TransferResource setContentLength( long contentLength )
+    {
+        this.contentLength = contentLength;
+        return this;
+    }
+
+    /**
+     * Gets the byte offset within the resource from which the download starts. A positive offset indicates a previous
+     * download attempt is being resumed, {@code 0} means the transfer starts at the first byte.
+     * 
+     * @return The zero-based index of the first byte being transferred, never negative.
+     */
+    public long getResumeOffset()
+    {
+        return resumeOffset;
+    }
+
+    /**
+     * Sets the byte offset within the resource at which the download starts.
+     * 
+     * @param resumeOffset The zero-based index of the first byte being transferred, must not be negative.
+     * @return This resource for chaining, never {@code null}.
+     */
+    public TransferResource setResumeOffset( long resumeOffset )
+    {
+        if ( resumeOffset < 0 )
+        {
+            throw new IllegalArgumentException( "resume offset cannot be negative" );
+        }
+        this.resumeOffset = resumeOffset;
+        return this;
+    }
+
+    /**
+     * Gets the timestamp when the transfer of this resource was started.
+     * 
+     * @return The timestamp when the transfer of this resource was started.
+     */
+    public long getTransferStartTime()
+    {
+        return startTime;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation during which this resource is
+     * transferred.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getRepositoryUrl() + getResourceName() + " <> " + getFile();
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/transfer/package-info.java b/org.argeo.slc.repo/src/org/eclipse/aether/transfer/package-info.java
new file mode 100644 (file)
index 0000000..5ce9ff3
--- /dev/null
@@ -0,0 +1,16 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+/**
+ * A listener and various exception types dealing with the transfer of a resource between the local system and a remote
+ * repository.
+ */
+package org.eclipse.aether.transfer;
+
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/version/InvalidVersionSpecificationException.java b/org.argeo.slc.repo/src/org/eclipse/aether/version/InvalidVersionSpecificationException.java
new file mode 100644 (file)
index 0000000..b3690c5
--- /dev/null
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.version;
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown when a version or version range could not be parsed.
+ */
+public class InvalidVersionSpecificationException
+    extends RepositoryException
+{
+
+    private final String version;
+
+    /**
+     * Creates a new exception with the specified version and detail message.
+     * 
+     * @param version The invalid version specification, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public InvalidVersionSpecificationException( String version, String message )
+    {
+        super( message );
+        this.version = version;
+    }
+
+    /**
+     * Creates a new exception with the specified version and cause.
+     * 
+     * @param version The invalid version specification, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public InvalidVersionSpecificationException( String version, Throwable cause )
+    {
+        super( "Could not parse version specification " + version + getMessage( ": ", cause ), cause );
+        this.version = version;
+    }
+
+    /**
+     * Creates a new exception with the specified version, detail message and cause.
+     * 
+     * @param version The invalid version specification, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public InvalidVersionSpecificationException( String version, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.version = version;
+    }
+
+    /**
+     * Gets the version or version range that could not be parsed.
+     * 
+     * @return The invalid version specification or {@code null} if unknown.
+     */
+    public String getVersion()
+    {
+        return version;
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/version/Version.java b/org.argeo.slc.repo/src/org/eclipse/aether/version/Version.java
new file mode 100644 (file)
index 0000000..4aceba6
--- /dev/null
@@ -0,0 +1,27 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.version;
+
+/**
+ * A parsed artifact version.
+ */
+public interface Version
+    extends Comparable<Version>
+{
+
+    /**
+     * Gets the original string representation of the version.
+     * 
+     * @return The string representation of the version, never {@code null}.
+     */
+    String toString();
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/version/VersionConstraint.java b/org.argeo.slc.repo/src/org/eclipse/aether/version/VersionConstraint.java
new file mode 100644 (file)
index 0000000..dcb3b68
--- /dev/null
@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.version;
+
+/**
+ * A constraint on versions for a dependency. A constraint can either consist of a version range (e.g. "[1, ]") or a
+ * single version (e.g. "1.1"). In the first case, the constraint expresses a hard requirement on a version matching the
+ * range. In the second case, the constraint expresses a soft requirement on a specific version (i.e. a recommendation).
+ */
+public interface VersionConstraint
+{
+
+    /**
+     * Gets the version range of this constraint.
+     * 
+     * @return The version range or {@code null} if none.
+     */
+    VersionRange getRange();
+
+    /**
+     * Gets the version recommended by this constraint.
+     * 
+     * @return The recommended version or {@code null} if none.
+     */
+    Version getVersion();
+
+    /**
+     * Determines whether the specified version satisfies this constraint. In more detail, a version satisfies this
+     * constraint if it matches its version range or if this constraint has no version range and the specified version
+     * equals the version recommended by the constraint.
+     * 
+     * @param version The version to test, must not be {@code null}.
+     * @return {@code true} if the specified version satisfies this constraint, {@code false} otherwise.
+     */
+    boolean containsVersion( Version version );
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/version/VersionRange.java b/org.argeo.slc.repo/src/org/eclipse/aether/version/VersionRange.java
new file mode 100644 (file)
index 0000000..cbc2405
--- /dev/null
@@ -0,0 +1,122 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2012 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.version;
+
+/**
+ * A range of versions.
+ */
+public interface VersionRange
+{
+
+    /**
+     * Determines whether the specified version is contained within this range.
+     * 
+     * @param version The version to test, must not be {@code null}.
+     * @return {@code true} if this range contains the specified version, {@code false} otherwise.
+     */
+    boolean containsVersion( Version version );
+
+    /**
+     * Gets a lower bound (if any) for this range. If existent, this range does not contain any version smaller than its
+     * lower bound. Note that complex version ranges might exclude some versions even within their bounds.
+     * 
+     * @return A lower bound for this range or {@code null} is there is none.
+     */
+    Bound getLowerBound();
+
+    /**
+     * Gets an upper bound (if any) for this range. If existent, this range does not contain any version greater than
+     * its upper bound. Note that complex version ranges might exclude some versions even within their bounds.
+     * 
+     * @return An upper bound for this range or {@code null} is there is none.
+     */
+    Bound getUpperBound();
+
+    /**
+     * A bound of a version range.
+     */
+    static final class Bound
+    {
+
+        private final Version version;
+
+        private final boolean inclusive;
+
+        /**
+         * Creates a new bound with the specified properties.
+         * 
+         * @param version The bounding version, must not be {@code null}.
+         * @param inclusive A flag whether the specified version is included in the range or not.
+         */
+        public Bound( Version version, boolean inclusive )
+        {
+            if ( version == null )
+            {
+                throw new IllegalArgumentException( "version missing" );
+            }
+            this.version = version;
+            this.inclusive = inclusive;
+        }
+
+        /**
+         * Gets the bounding version.
+         * 
+         * @return The bounding version, never {@code null}.
+         */
+        public Version getVersion()
+        {
+            return version;
+        }
+
+        /**
+         * Indicates whether the bounding version is included in the range or not.
+         * 
+         * @return {@code true} if the bounding version is included in the range, {@code false} if not.
+         */
+        public boolean isInclusive()
+        {
+            return inclusive;
+        }
+
+        @Override
+        public boolean equals( Object obj )
+        {
+            if ( obj == this )
+            {
+                return true;
+            }
+            else if ( obj == null || !getClass().equals( obj.getClass() ) )
+            {
+                return false;
+            }
+
+            Bound that = (Bound) obj;
+            return inclusive == that.inclusive && version.equals( that.version );
+        }
+
+        @Override
+        public int hashCode()
+        {
+            int hash = 17;
+            hash = hash * 31 + version.hashCode();
+            hash = hash * 31 + ( inclusive ? 1 : 0 );
+            return hash;
+        }
+
+        @Override
+        public String toString()
+        {
+            return String.valueOf( version );
+        }
+
+    }
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/version/VersionScheme.java b/org.argeo.slc.repo/src/org/eclipse/aether/version/VersionScheme.java
new file mode 100644 (file)
index 0000000..c19177a
--- /dev/null
@@ -0,0 +1,50 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.version;
+
+/**
+ * A version scheme that handles interpretation of version strings to facilitate their comparison.
+ */
+public interface VersionScheme
+{
+
+    /**
+     * Parses the specified version string, for example "1.0".
+     * 
+     * @param version The version string to parse, must not be {@code null}.
+     * @return The parsed version, never {@code null}.
+     * @throws InvalidVersionSpecificationException If the string violates the syntax rules of this scheme.
+     */
+    Version parseVersion( String version )
+        throws InvalidVersionSpecificationException;
+
+    /**
+     * Parses the specified version range specification, for example "[1.0,2.0)".
+     * 
+     * @param range The range specification to parse, must not be {@code null}.
+     * @return The parsed version range, never {@code null}.
+     * @throws InvalidVersionSpecificationException If the range specification violates the syntax rules of this scheme.
+     */
+    VersionRange parseVersionRange( String range )
+        throws InvalidVersionSpecificationException;
+
+    /**
+     * Parses the specified version constraint specification, for example "1.0" or "[1.0,2.0),(2.0,)".
+     * 
+     * @param constraint The constraint specification to parse, must not be {@code null}.
+     * @return The parsed version constraint, never {@code null}.
+     * @throws InvalidVersionSpecificationException If the constraint specification violates the syntax rules of this
+     *             scheme.
+     */
+    VersionConstraint parseVersionConstraint( final String constraint )
+        throws InvalidVersionSpecificationException;
+
+}
diff --git a/org.argeo.slc.repo/src/org/eclipse/aether/version/package-info.java b/org.argeo.slc.repo/src/org/eclipse/aether/version/package-info.java
new file mode 100644 (file)
index 0000000..7ef8c85
--- /dev/null
@@ -0,0 +1,15 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+/**
+ * The definition of a version scheme for parsing and comparing versions.
+ */
+package org.eclipse.aether.version;
+
diff --git a/org.argeo.slc.rpmfactory/.classpath b/org.argeo.slc.rpmfactory/.classpath
new file mode 100644 (file)
index 0000000..d499d30
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.slc.rpmfactory/.gitignore b/org.argeo.slc.rpmfactory/.gitignore
new file mode 100644 (file)
index 0000000..b83d222
--- /dev/null
@@ -0,0 +1 @@
+/target/
diff --git a/org.argeo.slc.rpmfactory/.project b/org.argeo.slc.rpmfactory/.project
new file mode 100644 (file)
index 0000000..29bc99a
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.slc.rpmfactory</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+               <nature>org.eclipse.pde.PluginNature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.slc.rpmfactory/META-INF/.gitignore b/org.argeo.slc.rpmfactory/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.slc.rpmfactory/bnd.bnd b/org.argeo.slc.rpmfactory/bnd.bnd
new file mode 100644 (file)
index 0000000..35c4c44
--- /dev/null
@@ -0,0 +1,5 @@
+Import-Package: javax.jcr.nodetype,\
+org.argeo.slc.repo,\
+org.osgi.*;version=0.0.0,\
+*
+                                                       
\ No newline at end of file
diff --git a/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/RpmFactory.java b/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/RpmFactory.java
new file mode 100644 (file)
index 0000000..0223a20
--- /dev/null
@@ -0,0 +1,62 @@
+package org.argeo.slc.rpmfactory;
+
+import java.io.File;
+import java.util.List;
+
+import javax.jcr.Node;
+
+/**
+ * Defines a build environment. This information is typically used by other
+ * components performing the various actions related to RPM build.
+ */
+public interface RpmFactory {
+       //
+       // DIRECT ACTIONS ON JCR REPOSITORY
+       //
+       public void indexWorkspace(String workspace);
+
+       public Node newDistribution(String distributionId);
+
+       //
+       // CONFIG FILES GENERATION
+       //
+       /** Creates a mock config file. */
+       public File getMockConfigFile(String arch, String branch);
+
+       /** Creates a yum config file. */
+       public File getYumRepoFile(String arch);
+
+       //
+       // WORKSPACES
+       //
+       public String getStagingWorkspace();
+
+       /**
+        * @return the name of the testing workspace, or null if and only if the
+        *         testing workspace was not enabled.
+        */
+       public String getTestingWorkspace();
+
+       public String getStableWorkspace();
+
+       public File getWorkspaceDir(String workspace);
+
+       //
+       // ARCH DEPENDENT INFOS
+       //
+       public List<String> getArchs();
+
+       public String getMockConfig(String arch);
+
+       public String getIdWithArch(String arch);
+
+       public File getResultDir(String arch);
+
+       //
+       // DEPLOYMENT
+       //
+       public String getGitBaseUrl();
+
+       public Boolean isDeveloperInstance();
+
+}
diff --git a/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/RpmProxyService.java b/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/RpmProxyService.java
new file mode 100644 (file)
index 0000000..40002a8
--- /dev/null
@@ -0,0 +1,7 @@
+package org.argeo.slc.rpmfactory;
+
+import org.argeo.jcr.proxy.ResourceProxy;
+
+/** Marker interface (useful for OSGi services references), may be extended later */
+public interface RpmProxyService extends ResourceProxy {
+}
diff --git a/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/RpmRepository.java b/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/RpmRepository.java
new file mode 100644 (file)
index 0000000..24d7c72
--- /dev/null
@@ -0,0 +1,9 @@
+package org.argeo.slc.rpmfactory;
+
+/** A YUM compatible repository of RPM packages. */
+public interface RpmRepository {
+       public String getId();
+
+       public String getUrl();
+
+}
diff --git a/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/AbstractRpmRepository.java b/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/AbstractRpmRepository.java
new file mode 100644 (file)
index 0000000..19418b6
--- /dev/null
@@ -0,0 +1,28 @@
+package org.argeo.slc.rpmfactory.core;
+
+import org.argeo.slc.rpmfactory.RpmRepository;
+
+/** Common method to RPM repositories. */
+public abstract class AbstractRpmRepository implements RpmRepository {
+       private String id;
+       private String url;
+
+       @Override
+       public String getId() {
+               return id;
+       }
+
+       @Override
+       public String getUrl() {
+               return url;
+       }
+
+       public void setId(String id) {
+               this.id = id;
+       }
+
+       public void setUrl(String url) {
+               this.url = url;
+       }
+
+}
diff --git a/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/BuildInMock.java b/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/BuildInMock.java
new file mode 100644 (file)
index 0000000..eab9ffa
--- /dev/null
@@ -0,0 +1,190 @@
+package org.argeo.slc.rpmfactory.core;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.exec.Executor;
+import org.apache.commons.io.FileUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.rpmfactory.RpmFactory;
+import org.argeo.slc.runtime.tasks.SystemCall;
+
+/** Build an RPM in mock. */
+public class BuildInMock implements Runnable {
+       private final static CmsLog log = CmsLog.getLog(BuildInMock.class);
+       private final static String NOARCH = "noarch";
+
+       private String rpmPackage = null;
+       private String branch = null;
+       private String arch = NOARCH;
+
+       private RpmFactory rpmFactory;
+       private Executor executor;
+
+       private String debuginfoDirName = "debuginfo";
+       private String mockExecutable = "/usr/bin/mock";
+
+       private List<String> preBuildCommands = new ArrayList<String>();
+
+       public void run() {
+               if (!rpmFactory.isDeveloperInstance()) {
+                       // clean/init
+                       SystemCall mockClean = createBaseMockCall();
+                       mockClean.arg("--init");
+                       mockClean.run();
+               }
+
+               // pre build commands
+               for (String preBuildCmd : preBuildCommands) {
+                       SystemCall mockClean = createBaseMockCall();
+                       mockClean.arg("--chroot").arg(preBuildCmd);
+                       mockClean.run();
+               }
+
+               // actual build
+               SystemCall mockBuild = createBaseMockCall();
+               mockBuild.arg("--scm-enable");
+               mockBuild.arg("--scm-option").arg("package=" + rpmPackage);
+               mockBuild.arg("--no-clean");
+               //
+               //
+               mockBuild.run();
+               //
+
+               // copy RPMs to target directories
+               File stagingDir = rpmFactory.getWorkspaceDir(rpmFactory
+                               .getStagingWorkspace());
+               File srpmDir = new File(stagingDir, "SRPMS");
+               srpmDir.mkdirs();
+               File archDir = null;
+               File debuginfoDir = null;
+               if (!arch.equals(NOARCH)) {
+                       archDir = new File(stagingDir, arch);
+                       debuginfoDir = new File(archDir, debuginfoDirName);
+                       debuginfoDir.mkdirs();
+               }
+
+               Set<File> reposToRecreate = new HashSet<File>();
+               File resultDir = rpmFactory.getResultDir(arch);
+               if (resultDir.exists())
+                       rpms: for (File file : resultDir.listFiles()) {
+                               if (file.isDirectory())
+                                       continue rpms;
+
+                               File[] targetDirs;
+                               if (file.getName().contains(".src.rpm"))
+                                       targetDirs = new File[] { srpmDir };
+                               else if (file.getName().contains("-debuginfo-"))
+                                       targetDirs = new File[] { debuginfoDir };
+                               else if (!arch.equals(NOARCH)
+                                               && file.getName().contains("." + arch + ".rpm"))
+                                       targetDirs = new File[] { archDir };
+                               else if (file.getName().contains(".noarch.rpm")) {
+                                       List<File> dirs = new ArrayList<File>();
+                                       for (String arch : rpmFactory.getArchs())
+                                               dirs.add(new File(stagingDir, arch));
+                                       targetDirs = dirs.toArray(new File[dirs.size()]);
+                               } else if (file.getName().contains(".rpm"))
+                                       throw new SlcException("Don't know where to copy " + file);
+                               else {
+                                       if (log.isTraceEnabled())
+                                               log.trace("Skip " + file);
+                                       continue rpms;
+                               }
+
+                               reposToRecreate.addAll(Arrays.asList(targetDirs));
+                               copyToDirs(file, targetDirs);
+                       }
+
+               // recreate changed repos
+               for (File repoToRecreate : reposToRecreate) {
+                       SystemCall createrepo = new SystemCall();
+                       createrepo.arg("createrepo");
+                       // sqllite db
+                       createrepo.arg("-d");
+                       // debuginfo
+                       if (!repoToRecreate.getName().equals(debuginfoDirName))
+                               createrepo.arg("-x").arg(debuginfoDirName + "/*");
+                       // quiet
+                       createrepo.arg("-q");
+                       createrepo.arg(repoToRecreate.getAbsolutePath());
+
+                       createrepo.setExecutor(executor);
+                       createrepo.run();
+                       log.info("Updated repo " + repoToRecreate);
+               }
+
+               // index staging workspace
+               rpmFactory.indexWorkspace(rpmFactory.getStagingWorkspace());
+       }
+
+       /** Creates a mock call with all the common options such as config file etc. */
+       protected SystemCall createBaseMockCall() {
+               String mockCfg = rpmFactory.getMockConfig(arch);
+               File mockConfigFile = rpmFactory.getMockConfigFile(arch, branch);
+
+               // prepare mock call
+               SystemCall mock = new SystemCall();
+
+               if (arch != null)
+                       mock.arg("setarch").arg(arch);
+               mock.arg(mockExecutable);
+               mock.arg("-v");
+               mock.arg("--configdir=" + mockConfigFile.getAbsoluteFile().getParent());
+               if (arch != null)
+                       mock.arg("--arch=" + arch);
+               mock.arg("-r").arg(mockCfg);
+
+               mock.setLogCommand(true);
+               mock.setExecutor(executor);
+
+               return mock;
+       }
+
+       protected void copyToDirs(File file, File[] dirs) {
+               for (File dir : dirs) {
+                       try {
+                               FileUtils.copyFileToDirectory(file, dir);
+                               if (log.isDebugEnabled())
+                                       log.debug(file + " => " + dir);
+                       } catch (IOException e) {
+                               throw new SlcException("Cannot copy " + file + " to " + dir, e);
+                       }
+               }
+       }
+
+       public void setArch(String arch) {
+               this.arch = arch;
+       }
+
+       public void setRpmPackage(String rpmPackage) {
+               this.rpmPackage = rpmPackage;
+       }
+
+       public void setBranch(String branch) {
+               this.branch = branch;
+       }
+
+       public void setRpmFactory(RpmFactory env) {
+               this.rpmFactory = env;
+       }
+
+       public void setExecutor(Executor executor) {
+               this.executor = executor;
+       }
+
+       public void setMockExecutable(String mockExecutable) {
+               this.mockExecutable = mockExecutable;
+       }
+
+       public void setPreBuildCommands(List<String> preBuildCommands) {
+               this.preBuildCommands = preBuildCommands;
+       }
+
+}
\ No newline at end of file
diff --git a/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/CreateRpmDistribution.java b/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/CreateRpmDistribution.java
new file mode 100644 (file)
index 0000000..43c6c6c
--- /dev/null
@@ -0,0 +1,141 @@
+package org.argeo.slc.rpmfactory.core;
+
+import java.io.File;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.StringTokenizer;
+
+import javax.jcr.Node;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.rpmfactory.RpmFactory;
+import org.argeo.slc.runtime.tasks.SystemCall;
+
+/**
+ * Gather RPMs from various sources (local builds or third party) into a
+ * consistent distributable set (typically to be used to generate an ISO).
+ */
+public class CreateRpmDistribution implements Runnable {
+       private final static CmsLog log = CmsLog
+                       .getLog(CreateRpmDistribution.class);
+
+       private RpmFactory rpmFactory;
+       private RpmDistribution rpmDistribution;
+
+       private String arch = "x86_64";
+
+       private String repoqueryExecutable = "/usr/bin/repoquery";
+
+       @Override
+       public void run() {
+               Session session = null;
+               // Reader reader = null;
+               try {
+                       Node baseFolder = rpmFactory.newDistribution(rpmDistribution
+                                       .getId());
+                       session = baseFolder.getSession();
+                       Node targetFolder = baseFolder.addNode(arch, NodeType.NT_FOLDER);
+
+                       SystemCall repoquery = new SystemCall();
+                       repoquery.arg(repoqueryExecutable);
+
+                       File yumConfigFile = rpmFactory.getYumRepoFile(arch);
+                       repoquery.arg("-c", yumConfigFile.getAbsolutePath());
+                       repoquery.arg("--requires");
+                       repoquery.arg("--resolve");
+                       repoquery.arg("--location");
+                       repoquery.arg("--archlist=" + arch);
+
+                       for (String rpmPackage : rpmDistribution.getPackages())
+                               repoquery.arg(rpmPackage);
+
+                       if (log.isDebugEnabled())
+                               log.debug("Command:\n" + repoquery.asCommand());
+
+                       String output = repoquery.function();
+
+                       if (log.isDebugEnabled())
+                               log.debug(output + "\n");
+                       // reader = new StringReader(output);
+                       StringTokenizer lines = new StringTokenizer(output, "\n");
+                       // List<String> dependencies = IOUtils.readLines(reader);
+                       dependencies: while (lines.hasMoreTokens()) {
+                               String urlStr = lines.nextToken();
+                               InputStream in = null;
+                               try {
+                                       URL url = new URL(urlStr);
+                                       String fileName = FilenameUtils.getName(url.getFile());
+                                       String[] tokens = fileName.split("-");
+                                       if (tokens.length < 3)
+                                               continue dependencies;
+                                       StringBuilder buf = new StringBuilder();
+                                       for (int i = 0; i < tokens.length - 2; i++) {
+                                               if (i != 0)
+                                                       buf.append('-');
+                                               buf.append(tokens[i]);
+
+                                       }
+                                       String packageName = buf.toString();
+                                       for (RpmPackageSet excluded : rpmDistribution
+                                                       .getExcludedPackages()) {
+                                               if (excluded.contains(packageName)) {
+                                                       if (log.isDebugEnabled())
+                                                               log.debug("Skipped " + packageName);
+                                                       continue dependencies;// skip
+                                               }
+                                       }
+                                       in = url.openStream();
+                                       JcrUtils.copyStreamAsFile(targetFolder, fileName, in);
+                                       targetFolder.getSession().save();
+                                       if (log.isDebugEnabled())
+                                               log.debug("Copied  " + packageName);
+                               } catch (Exception e) {
+                                       log.error("Cannot copy " + urlStr, e);
+                               } finally {
+                                       IOUtils.closeQuietly(in);
+                               }
+                       }
+
+                       // createrepo
+                       File workspaceDir = rpmFactory.getWorkspaceDir(rpmDistribution
+                                       .getId());
+                       SystemCall createrepo = new SystemCall();
+                       createrepo.arg("createrepo");
+                       createrepo.arg("-q");
+                       createrepo.arg("-d");
+                       File archDir = new File(workspaceDir.getPath()
+                                       + targetFolder.getPath());
+                       createrepo.arg(archDir.getAbsolutePath());
+                       createrepo.run();
+               } catch (Exception e) {
+                       throw new SlcException("Cannot generate distribution "
+                                       + rpmDistribution.getId(), e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+                       // IOUtils.closeQuietly(reader);
+               }
+       }
+
+       public void setRpmDistribution(RpmDistribution rpmDistribution) {
+               this.rpmDistribution = rpmDistribution;
+       }
+
+       public void setRpmFactory(RpmFactory rpmFactory) {
+               this.rpmFactory = rpmFactory;
+       }
+
+       public void setArch(String arch) {
+               this.arch = arch;
+       }
+
+       public void setRepoqueryExecutable(String yumdownloaderExecutable) {
+               this.repoqueryExecutable = yumdownloaderExecutable;
+       }
+
+}
diff --git a/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/ReleaseStaging.java b/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/ReleaseStaging.java
new file mode 100644 (file)
index 0000000..78f7af3
--- /dev/null
@@ -0,0 +1,108 @@
+package org.argeo.slc.rpmfactory.core;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.exec.Executor;
+import org.apache.commons.io.FileUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.rpmfactory.RpmFactory;
+import org.argeo.slc.runtime.tasks.SystemCall;
+
+/** Releases the content of staging to a public repository. */
+public class ReleaseStaging implements Runnable {
+       private final static CmsLog log = CmsLog.getLog(ReleaseStaging.class);
+
+       private RpmFactory rpmFactory;
+       private Executor executor;
+
+       private String debuginfoDirName = "debuginfo";
+
+       @Override
+       public void run() {
+               String sourceWorkspace = rpmFactory.getStagingWorkspace();
+               File sourceRepoDir = rpmFactory.getWorkspaceDir(sourceWorkspace);
+               String targetWorkspace = rpmFactory.getTestingWorkspace() != null ? rpmFactory
+                               .getTestingWorkspace() : rpmFactory.getStableWorkspace();
+               File targetRepoDir = rpmFactory.getWorkspaceDir(targetWorkspace);
+               List<File> reposToRecreate = new ArrayList<File>();
+
+               stagingChildren: for (File dir : sourceRepoDir.listFiles()) {
+                       if (!dir.isDirectory())
+                               continue stagingChildren;
+                       if (dir.getName().equals("lost+found"))
+                               continue stagingChildren;
+
+                       File targetDir = new File(targetRepoDir, dir.getName());
+                       try {
+                               FileUtils.copyDirectory(dir, targetDir);
+                               if (log.isDebugEnabled())
+                                       log.debug(dir + " => " + targetDir);
+                       } catch (IOException e) {
+                               throw new SlcException(sourceRepoDir
+                                               + " could not be copied properly, check it manually."
+                                               + " Metadata have NOT been updated.", e);
+                       }
+
+                       reposToRecreate.add(dir);
+                       reposToRecreate.add(targetDir);
+                       File debugInfoDir = new File(dir, debuginfoDirName);
+                       if (debugInfoDir.exists())
+                               reposToRecreate.add(debugInfoDir);
+                       File targetDebugInfoDir = new File(targetDir, debuginfoDirName);
+                       if (targetDebugInfoDir.exists())
+                               reposToRecreate.add(targetDebugInfoDir);
+
+               }
+
+               // clear staging
+               for (File dir : sourceRepoDir.listFiles()) {
+                       try {
+                               if (dir.getName().equals("lost+found"))
+                                       continue;
+                               if (dir.isDirectory())
+                                       FileUtils.deleteDirectory(dir);
+                       } catch (IOException e) {
+                               log.error("Could not delete " + dir + ". " + e);
+                       }
+               }
+
+               // recreate changed repos
+               for (File repoToRecreate : reposToRecreate) {
+                       repoToRecreate.mkdirs();
+                       SystemCall createrepo = new SystemCall();
+                       createrepo.arg("createrepo");
+                       // sqllite db
+                       createrepo.arg("-d");
+                       // debuginfo
+                       if (!repoToRecreate.getName().equals(debuginfoDirName))
+                               createrepo.arg("-x").arg(debuginfoDirName + "/*");
+                       // quiet
+                       createrepo.arg("-q");
+                       createrepo.arg(repoToRecreate.getAbsolutePath());
+
+                       createrepo.setExecutor(executor);
+                       createrepo.run();
+                       log.info("Updated repo " + repoToRecreate);
+               }
+
+               rpmFactory.indexWorkspace(sourceWorkspace);
+               rpmFactory.indexWorkspace(targetWorkspace);
+       }
+
+       public void setRpmFactory(RpmFactory rpmFactory) {
+               this.rpmFactory = rpmFactory;
+       }
+
+       public void setExecutor(Executor executor) {
+               this.executor = executor;
+       }
+
+       public void setDebuginfoDirName(String debuginfoDirName) {
+               this.debuginfoDirName = debuginfoDirName;
+       }
+
+}
diff --git a/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmDistribution.java b/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmDistribution.java
new file mode 100644 (file)
index 0000000..0df31c6
--- /dev/null
@@ -0,0 +1,35 @@
+package org.argeo.slc.rpmfactory.core;
+
+import java.util.List;
+
+/** A consistent distributable set of RPM. */
+public class RpmDistribution {
+       private String id;
+       private List<String> packages;
+       private List<RpmPackageSet> excludedPackages;
+
+       public List<String> getPackages() {
+               return packages;
+       }
+
+       public void setPackages(List<String> packages) {
+               this.packages = packages;
+       }
+
+       public String getId() {
+               return id;
+       }
+
+       public void setId(String id) {
+               this.id = id;
+       }
+
+       public List<RpmPackageSet> getExcludedPackages() {
+               return excludedPackages;
+       }
+
+       public void setExcludedPackages(List<RpmPackageSet> excludedPackages) {
+               this.excludedPackages = excludedPackages;
+       }
+
+}
diff --git a/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmFactoryImpl.java b/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmFactoryImpl.java
new file mode 100644 (file)
index 0000000..47ca4da
--- /dev/null
@@ -0,0 +1,456 @@
+package org.argeo.slc.rpmfactory.core;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.commons.io.FileUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.slc.SlcConstants;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.repo.NodeIndexerVisitor;
+import org.argeo.slc.rpmfactory.RpmFactory;
+import org.argeo.slc.rpmfactory.RpmRepository;
+import org.argeo.slc.runtime.tasks.SystemCall;
+
+/**
+ * Defines a build environment. This information is typically used by other
+ * components performing the various actions related to RPM build.
+ */
+public class RpmFactoryImpl implements RpmFactory {
+       private CmsLog log = CmsLog.getLog(RpmFactoryImpl.class);
+
+       private Repository rpmRepository;
+       private Repository distRepository;
+
+       private String id;
+       private List<RpmRepository> repositories = new ArrayList<RpmRepository>();
+       private List<String> archs = new ArrayList<String>();
+
+       private String rpmBase = "/mnt/slc/repos/rpm";
+       private String distBase = "/mnt/slc/repos/dist";
+       private String mockVar = "/var/lib/mock";
+       private String mockEtc = "/etc/mock";
+
+       private DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmm");
+
+       private String gitWorkspace = "git";
+
+       private String localUrlBase = "http://localhost:7070/";
+       /** If not null or empty, this is a developer instance. */
+       private String gitDevBaseUrl = null;
+
+       private Boolean withTestingRepository = false;
+
+       private String yumConfigMainSection = "cachedir=/var/cache/yum\n"
+                       + "debuglevel=1\n" + "reposdir=/dev/null\n"
+                       + "logfile=/var/log/yum.log\n" + "retries=20\n" + "obsoletes=1\n"
+                       + "gpgcheck=0\n" + "assumeyes=1\n" + "syslog_ident=mock\n"
+                       + "syslog_device=\n" + "http_caching=none\n";
+
+       private String defaultMacroFiles = "/usr/lib/rpm/macros:"
+                       + "/usr/lib/rpm/ia32e-linux/macros:"
+                       + "/usr/lib/rpm/redhat/macros:" + "/etc/rpm/macros.*:"
+                       + "/etc/rpm/macros:" + "/etc/rpm/ia32e-linux/macros:"
+                       + "~/.rpmmacros";
+       private Map<String, String> rpmmacros = new HashMap<String, String>();
+
+       // set by init
+       private String proxiedReposBase;
+       private String managedReposBase;
+
+       private String stagingWorkspace;
+       private String testingWorkspace;
+       private String stableWorkspace;
+
+       private File rpmFactoryBaseDir;
+       private File mockConfDir;
+       private File yumConfDir;
+
+       public void init() {
+               // local URL bases
+               proxiedReposBase = localUrlBase + "repo/rpm/";
+               managedReposBase = localUrlBase + "data/public/rpm/";
+
+               // local directories
+               rpmFactoryBaseDir.mkdirs();
+               mockConfDir = new File(rpmFactoryBaseDir.getPath() + "/conf/mock");
+               mockConfDir.mkdirs();
+               yumConfDir = new File(rpmFactoryBaseDir.getPath() + "/conf/yum");
+               yumConfDir.mkdirs();
+
+               // managed repositories
+               stagingWorkspace = id + "-staging";
+               if (withTestingRepository)
+                       testingWorkspace = id + "-testing";
+               stableWorkspace = id;
+
+               initDistWorkspace(stableWorkspace);
+               initGitWorkspace();
+               initRpmWorkspace(stagingWorkspace);
+               if (withTestingRepository)
+                       initRpmWorkspace(testingWorkspace);
+               initRpmWorkspace(stableWorkspace);
+       }
+
+       protected void initRpmWorkspace(String workspace) {
+               Session session = null;
+               try {
+                       session = JcrUtils.loginOrCreateWorkspace(rpmRepository, workspace);
+                       JcrUtils.addPrivilege(session, "/", "anonymous", "jcr:read");
+                       JcrUtils.addPrivilege(session, "/", SlcConstants.ROLE_SLC,
+                                       "jcr:all");
+
+                       for (String arch : archs) {
+                               Node archFolder = JcrUtils.mkfolders(session, "/" + arch);
+                               session.save();
+                               File workspaceDir = getWorkspaceDir(workspace);
+                               try {
+                                       if (!archFolder.hasNode("repodata")) {
+                                               // touch a file in order to make sure this is properly
+                                               // mounted.
+                                               File touch = new File(workspaceDir, ".touch");
+                                               touch.createNewFile();
+                                               touch.delete();
+
+                                               SystemCall createrepo = new SystemCall();
+                                               createrepo.arg("createrepo");
+                                               createrepo.arg("-q");
+                                               File archDir = new File(workspaceDir, arch);
+                                               createrepo.arg(archDir.getAbsolutePath());
+                                               createrepo.run();
+                                       }
+                               } catch (IOException e) {
+                                       log.error(workspaceDir + " not properly mounted.", e);
+                               }
+                       }
+               } catch (Exception e) {
+                       throw new SlcException("Cannot initialize workspace " + workspace,
+                                       e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+       /** Caller must logout the underlying session. */
+       public Node newDistribution(String distributionId) {
+               Session session = null;
+               try {
+                       session = JcrUtils.loginOrCreateWorkspace(rpmRepository,
+                                       distributionId);
+                       JcrUtils.addPrivilege(session, "/", "anonymous", "jcr:read");
+                       JcrUtils.addPrivilege(session, "/", SlcConstants.ROLE_SLC,
+                                       "jcr:all");
+
+                       Calendar now = new GregorianCalendar();
+                       String folderName = dateFormat.format(now.getTime());
+                       return JcrUtils.mkfolders(session, "/" + folderName);
+               } catch (Exception e) {
+                       JcrUtils.logoutQuietly(session);
+                       throw new SlcException("Cannot initialize distribution workspace "
+                                       + distributionId, e);
+               }
+       }
+
+       protected void initGitWorkspace() {
+               Session session = null;
+               try {
+                       session = JcrUtils.loginOrCreateWorkspace(rpmRepository,
+                                       gitWorkspace);
+                       JcrUtils.addPrivilege(session, "/", "anonymous", "jcr:read");
+                       JcrUtils.addPrivilege(session, "/", SlcConstants.ROLE_SLC,
+                                       "jcr:all");
+               } catch (Exception e) {
+                       throw new SlcException("Cannot initialize workspace "
+                                       + gitWorkspace, e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+       protected void initDistWorkspace(String workspace) {
+               Session session = null;
+               try {
+                       session = JcrUtils
+                                       .loginOrCreateWorkspace(distRepository, workspace);
+                       JcrUtils.addPrivilege(session, "/", "anonymous", "jcr:read");
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot initialize workspace " + workspace,
+                                       e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+       public void destroy() {
+
+       }
+
+       public String generateMockConfigFile(String arch, String branch) {
+               StringBuffer buf = new StringBuffer();
+
+               buf.append("config_opts['root'] = '" + getIdWithArch(arch) + "'\n");
+               buf.append("config_opts['target_arch'] = '" + arch + "'\n");
+               buf.append("config_opts['legal_host_arches'] = ('" + arch + "',)\n");
+               buf.append("config_opts['chroot_setup_cmd'] = 'groupinstall buildsys-build'\n");
+               // buf.append("config_opts['dist'] = 'el6'\n");
+               buf.append("config_opts['plugin_conf']['yum_cache_enable'] = False\n");
+
+               buf.append("config_opts['scm'] = False\n");
+               buf.append("config_opts['scm_opts']['method'] = 'git'\n");
+               buf.append("config_opts['scm_opts']['spec'] = 'SCM_PKG.spec'\n");
+               buf.append("config_opts['scm_opts']['ext_src_dir'] = '"
+                               + getSourcesDir().getAbsolutePath() + "'\n");
+               buf.append("config_opts['scm_opts']['git_timestamps'] = True\n");
+
+               // development
+               if (gitDevBaseUrl != null && !gitDevBaseUrl.trim().equals(""))
+                       buf.append("config_opts['scm_opts']['git_get'] = 'git clone "
+                                       + (branch != null ? "-b " + branch : "") + " "
+                                       + gitDevBaseUrl + "/SCM_PKG SCM_PKG'\n");
+               else
+                       buf.append("config_opts['scm_opts']['git_get'] = 'git clone "
+                                       + (branch != null ? "-b " + branch : "") + " "
+                                       + getGitBaseUrl() + "/SCM_PKG.git SCM_PKG'\n");
+
+               buf.append("\nconfig_opts['yum.conf'] = \"\"\"\n");
+               buf.append(generateYumConfigFile(arch)).append('\n');
+               buf.append("\"\"\"\n");
+               return buf.toString();
+       }
+
+       public String generateYumConfigFile(String arch) {
+               StringBuffer buf = new StringBuffer();
+               buf.append("[main]\n");
+               buf.append(yumConfigMainSection).append('\n');
+
+               for (RpmRepository repository : repositories) {
+                       buf.append('[').append(repository.getId()).append("]\n");
+                       buf.append("name=").append(repository.getId()).append('\n');
+                       if (repository instanceof ThirdPartyRpmRepository) {
+                               buf.append("#baseurl=").append(repository.getUrl())
+                                               .append(arch).append('/').append("\n");
+                               buf.append("baseurl=").append(proxiedReposBase)
+                                               .append(repository.getId()).append('/').append(arch)
+                                               .append('/').append("\n");
+                               if (((ThirdPartyRpmRepository) repository).getYumConf() != null)
+                                       buf.append(
+                                                       ((ThirdPartyRpmRepository) repository).getYumConf()
+                                                                       .trim()).append('\n');
+                       }
+               }
+
+               // managed repos
+               addManagedRepository(buf, stagingWorkspace, arch);
+               if (withTestingRepository)
+                       addManagedRepository(buf, testingWorkspace, arch);
+               addManagedRepository(buf, stableWorkspace, arch);
+               return buf.toString();
+       }
+
+       protected void addManagedRepository(StringBuffer buf, String workspace,
+                       String arch) {
+               buf.append('[').append(workspace).append("]\n");
+               buf.append("baseurl=").append(managedReposBase).append(workspace)
+                               .append('/').append(arch).append('/').append("\n");
+               buf.append("gpgcheck=0").append("\n");
+       }
+
+       /** Creates a mock config file. */
+       public File getMockConfigFile(String arch, String branch) {
+               File mockSiteDefaultsFile = new File(mockConfDir, "site-defaults.cfg");
+               File mockLoggingFile = new File(mockConfDir, "logging.ini");
+               File mockConfigFile = new File(mockConfDir, getIdWithArch(arch)
+                               + ".cfg");
+               try {
+                       if (!mockSiteDefaultsFile.exists())
+                               mockSiteDefaultsFile.createNewFile();
+                       if (!mockLoggingFile.exists())
+                               FileUtils.copyFile(new File(mockEtc + "/logging.ini"),
+                                               mockLoggingFile);
+
+                       FileUtils.writeStringToFile(mockConfigFile,
+                                       generateMockConfigFile(arch, branch));
+                       return mockConfigFile;
+               } catch (IOException e) {
+                       throw new SlcException("Cannot write mock config file to "
+                                       + mockConfigFile, e);
+               }
+       }
+
+       /** Creates a yum config file. */
+       public File getYumRepoFile(String arch) {
+               File yumConfigFile = new File(yumConfDir, getIdWithArch(arch) + ".repo");
+               try {
+                       FileUtils.writeStringToFile(yumConfigFile,
+                                       generateYumConfigFile(arch));
+                       return yumConfigFile;
+               } catch (IOException e) {
+                       throw new SlcException("Cannot write yum config file to "
+                                       + yumConfigFile, e);
+               }
+       }
+
+       public File getResultDir(String arch) {
+               return new File(mockVar + "/" + getIdWithArch(arch) + "/result");
+       }
+
+       public File getWorkspaceDir(String workspace) {
+               return new File(rpmBase + "/" + workspace);
+       }
+
+       public File getSourcesDir() {
+               return new File(distBase + "/" + stableWorkspace);
+       }
+
+       public String getMockConfig(String arch) {
+               return getIdWithArch(arch);
+       }
+
+       public String getIdWithArch(String arch) {
+               return id + "-" + arch;
+       }
+
+       public String getGitBaseUrl() {
+               return managedReposBase + gitWorkspace;
+       }
+
+       public void indexWorkspace(String workspace) {
+               Session session = null;
+               try {
+                       session = rpmRepository.login(workspace);
+                       session.getRootNode().accept(
+                                       new NodeIndexerVisitor(new RpmIndexer()));
+                       if (log.isDebugEnabled())
+                               log.debug("Indexed workspace " + workspace);
+               } catch (RepositoryException e) {
+                       throw new SlcException("Cannot index workspace " + workspace, e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+       public Boolean isDeveloperInstance() {
+               return gitDevBaseUrl != null;
+       }
+
+       /** Write (topdir)/rpmmacros and (topdir)/rpmrc */
+       public void writeRpmbuildConfigFiles(File topdir) {
+               writeRpmbuildConfigFiles(topdir, new File(topdir, "rpmmacros"),
+                               new File(topdir, "rpmrc"));
+       }
+
+       public void writeRpmbuildConfigFiles(File topdir, File rpmmacroFile,
+                       File rpmrcFile) {
+               try {
+                       List<String> macroLines = new ArrayList<String>();
+                       macroLines.add("%_topdir " + topdir.getCanonicalPath());
+                       for (String macroKey : rpmmacros.keySet()) {
+                               macroLines.add(macroKey + " " + rpmmacros.get(macroKey));
+                       }
+                       FileUtils.writeLines(rpmmacroFile, macroLines);
+
+                       List<String> rpmrcLines = new ArrayList<String>();
+                       rpmrcLines.add("include: /usr/lib/rpm/rpmrc");
+                       rpmrcLines.add("macrofiles: " + defaultMacroFiles + ":"
+                                       + rpmmacroFile.getCanonicalPath());
+                       FileUtils.writeLines(rpmrcFile, rpmrcLines);
+               } catch (IOException e) {
+                       throw new SlcException("Cannot write rpmbuild config files", e);
+               }
+
+       }
+
+       public Map<String, String> getRpmmacros() {
+               return rpmmacros;
+       }
+
+       public void setRpmmacros(Map<String, String> rpmmacros) {
+               this.rpmmacros = rpmmacros;
+       }
+
+       public String getDefaultMacroFiles() {
+               return defaultMacroFiles;
+       }
+
+       public void setDefaultMacroFiles(String defaultMacroFiles) {
+               this.defaultMacroFiles = defaultMacroFiles;
+       }
+
+       public void setArchs(List<String> archs) {
+               this.archs = archs;
+       }
+
+       public List<String> getArchs() {
+               return archs;
+       }
+
+       public void setRpmBase(String stagingBase) {
+               this.rpmBase = stagingBase;
+       }
+
+       public void setId(String id) {
+               this.id = id;
+       }
+
+       public void setMockVar(String mockVar) {
+               this.mockVar = mockVar;
+       }
+
+       public void setRpmRepository(Repository rpmRepository) {
+               this.rpmRepository = rpmRepository;
+       }
+
+       public void setDistRepository(Repository distRepository) {
+               this.distRepository = distRepository;
+       }
+
+       public void setLocalUrlBase(String localUrlBase) {
+               this.localUrlBase = localUrlBase;
+       }
+
+       public void setYumConfigMainSection(String yumConfigMainSection) {
+               this.yumConfigMainSection = yumConfigMainSection;
+       }
+
+       public void setRepositories(List<RpmRepository> repositories) {
+               this.repositories = repositories;
+       }
+
+       public void setRpmFactoryBaseDir(File rpmFactoryBaseDir) {
+               this.rpmFactoryBaseDir = rpmFactoryBaseDir;
+       }
+
+       public String getStagingWorkspace() {
+               return stagingWorkspace;
+       }
+
+       public String getTestingWorkspace() {
+               return testingWorkspace;
+       }
+
+       public String getStableWorkspace() {
+               return stableWorkspace;
+       }
+
+       public void setWithTestingRepository(Boolean withTestingRepository) {
+               this.withTestingRepository = withTestingRepository;
+       }
+
+       public void setGitDevBaseUrl(String gitBaseUrl) {
+               this.gitDevBaseUrl = gitBaseUrl;
+       }
+}
diff --git a/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmIndexer.java b/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmIndexer.java
new file mode 100644 (file)
index 0000000..ae9e70c
--- /dev/null
@@ -0,0 +1,117 @@
+package org.argeo.slc.rpmfactory.core;
+
+import static org.redline_rpm.header.Header.HeaderTag.HEADERIMMUTABLE;
+import static org.redline_rpm.header.Signature.SignatureTag.SIGNATURES;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.commons.io.FilenameUtils;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.argeo.slc.repo.NodeIndexer;
+import org.redline_rpm.ChannelWrapper.Key;
+import org.redline_rpm.ReadableChannelWrapper;
+import org.redline_rpm.header.AbstractHeader;
+import org.redline_rpm.header.Format;
+import org.redline_rpm.header.Header;
+
+/** Indexes an RPM file. */
+public class RpmIndexer implements NodeIndexer, SlcNames {
+       private Boolean force = false;
+
+       @Override
+       public Boolean support(String path) {
+               return FilenameUtils.getExtension(path).equals("rpm");
+       }
+
+       @Override
+       public void index(Node node) {
+               try {
+                       if (!support(node.getPath()))
+                               return;
+
+                       // Already indexed
+                       if (!force && node.isNodeType(SlcTypes.SLC_RPM))
+                               return;
+
+                       if (!node.isNodeType(NodeType.NT_FILE))
+                               return;
+
+                       InputStream in = node.getNode(Node.JCR_CONTENT)
+                                       .getProperty(Property.JCR_DATA).getBinary().getStream();
+                       ReadableChannelWrapper channel = new ReadableChannelWrapper(
+                                       Channels.newChannel(in));
+                       Format format = readRpmInfo(channel);
+
+                       node.addMixin(SlcTypes.SLC_RPM);
+                       node.setProperty(SLC_NAME, readTag(format, Header.HeaderTag.NAME));
+                       String rpmVersion = readTag(format, Header.HeaderTag.VERSION);
+                       String rpmRelease = readTag(format, Header.HeaderTag.RELEASE);
+                       node.setProperty(SLC_RPM_VERSION, rpmVersion);
+                       node.setProperty(SLC_RPM_RELEASE, rpmRelease);
+                       node.setProperty(SLC_VERSION, rpmVersion + "-" + rpmRelease);
+
+                       String arch = readTag(format, Header.HeaderTag.ARCH);
+                       if (arch != null)
+                               node.setProperty(SLC_RPM_ARCH, arch);
+
+                       String archiveSize = readTag(format, Header.HeaderTag.ARCHIVESIZE);
+                       if (archiveSize != null)
+                               node.setProperty(SLC_RPM_ARCHIVE_SIZE,
+                                               Long.parseLong(archiveSize));
+
+                       node.getSession().save();
+               } catch (Exception e) {
+                       throw new SlcException("Cannot index " + node, e);
+               }
+
+       }
+
+       @SuppressWarnings("unused")
+       public Format readRpmInfo(ReadableChannelWrapper channel) throws Exception {
+               Format format = new Format();
+
+               Key<Integer> lead = channel.start();
+               format.getLead().read(channel);
+               // System.out.println( "Lead ended at '" + in.finish( lead) + "'.");
+
+               Key<Integer> signature = channel.start();
+               int count = format.getSignature().read(channel);
+               int expected = ByteBuffer
+                               .wrap((byte[]) format.getSignature().getEntry(SIGNATURES)
+                                               .getValues(), 8, 4).getInt()
+                               / -16;
+               // System.out.println( "Signature ended at '" + in.finish( signature) +
+               // "' and contained '" + count + "' headers (expected '" + expected +
+               // "').");
+
+               Key<Integer> header = channel.start();
+               count = format.getHeader().read(channel);
+               expected = ByteBuffer.wrap(
+                               (byte[]) format.getHeader().getEntry(HEADERIMMUTABLE)
+                                               .getValues(), 8, 4).getInt()
+                               / -16;
+               // System.out.println( "Header ended at '" + in.finish( header) +
+               // " and contained '" + count + "' headers (expected '" + expected +
+               // "').");
+
+               return format;
+       }
+
+       private String readTag(Format format, Header.HeaderTag tag) {
+               AbstractHeader.Entry<?> entry = format.getHeader().getEntry(tag);
+               if (entry == null)
+                       return null;
+               if (entry.getValues() == null)
+                       return null;
+               Object[] values = (Object[]) entry.getValues();
+               return values[0].toString().trim();
+       }
+}
diff --git a/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmPackageSet.java b/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmPackageSet.java
new file mode 100644 (file)
index 0000000..1693413
--- /dev/null
@@ -0,0 +1,6 @@
+package org.argeo.slc.rpmfactory.core;
+
+/** Set of RPM packages */
+public interface RpmPackageSet {
+       public Boolean contains(String rpmPackage);
+}
diff --git a/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmProxyServiceImpl.java b/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmProxyServiceImpl.java
new file mode 100644 (file)
index 0000000..0772c98
--- /dev/null
@@ -0,0 +1,141 @@
+package org.argeo.slc.rpmfactory.core;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.security.AccessControlException;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.ArgeoNames;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.jcr.proxy.AbstractUrlProxy;
+import org.argeo.slc.SlcConstants;
+import org.argeo.slc.SlcException;
+import org.argeo.slc.SlcNames;
+import org.argeo.slc.SlcTypes;
+import org.argeo.slc.repo.RepoConstants;
+import org.argeo.slc.rpmfactory.RpmProxyService;
+import org.argeo.slc.rpmfactory.RpmRepository;
+
+/** Synchronises the node repository with remote Maven repositories */
+public class RpmProxyServiceImpl extends AbstractUrlProxy implements
+               RpmProxyService, ArgeoNames, SlcNames {
+       private final static CmsLog log = CmsLog.getLog(RpmProxyServiceImpl.class);
+
+       private Set<RpmRepository> defaultRepositories = new HashSet<RpmRepository>();
+
+       @Override
+       protected void beforeInitSessionSave(Session session)
+                       throws RepositoryException {
+               JcrUtils.addPrivilege(session, "/", "anonymous", "jcr:read");
+               try {
+                       JcrUtils.addPrivilege(session, "/", SlcConstants.ROLE_SLC,
+                                       "jcr:all");
+               } catch (AccessControlException e) {
+                       if (log.isTraceEnabled())
+                               log.trace("Cannot give jcr:all privileges to "+SlcConstants.ROLE_SLC);
+               }
+
+               JcrUtils.mkdirsSafe(session, RepoConstants.PROXIED_REPOSITORIES);
+       }
+
+       /**
+        * Retrieve and add this file to the repository
+        */
+       @Override
+       protected Node retrieve(Session session, String path) {
+               StringBuilder relativePathBuilder = new StringBuilder();
+               String repoId = extractRepoId(path, relativePathBuilder);
+               // remove starting '/'
+               String relativePath = relativePathBuilder.toString().substring(1);
+
+               RpmRepository sourceRepo = null;
+               for (Iterator<RpmRepository> reposIt = defaultRepositories.iterator(); reposIt
+                               .hasNext();) {
+                       RpmRepository rpmRepo = reposIt.next();
+                       if (rpmRepo.getId().equals(repoId)) {
+                               sourceRepo = rpmRepo;
+                               break;
+                       }
+               }
+
+               if (sourceRepo == null)
+                       throw new SlcException("No RPM repository found for " + path);
+
+               try {
+                       String baseUrl = sourceRepo.getUrl();
+                       String remoteUrl = baseUrl + relativePath;
+                       Node node = proxyUrl(session, remoteUrl, path);
+                       if (node != null) {
+                               registerSource(sourceRepo, node, remoteUrl);
+                               if (log.isDebugEnabled())
+                                       log.debug("Imported " + remoteUrl + " to " + node);
+                               return node;
+                       }
+               } catch (Exception e) {
+                       throw new SlcException("Cannot proxy " + path, e);
+               }
+               JcrUtils.discardQuietly(session);
+               throw new SlcException("No proxy found for " + path);
+       }
+
+       protected void registerSource(RpmRepository sourceRepo, Node node,
+                       String remoteUrl) throws RepositoryException {
+               node.addMixin(SlcTypes.SLC_KNOWN_ORIGIN);
+               Node origin;
+               if (!node.hasNode(SLC_ORIGIN))
+                       origin = node.addNode(SLC_ORIGIN, SlcTypes.SLC_PROXIED);
+               else
+                       origin = node.getNode(SLC_ORIGIN);
+
+               // proxied repository
+               Node proxiedRepository;
+               String proxiedRepositoryPath = RepoConstants.PROXIED_REPOSITORIES + '/'
+                               + sourceRepo.getId();
+               Session session = node.getSession();
+               if (session.itemExists(proxiedRepositoryPath)) {
+                       proxiedRepository = session.getNode(proxiedRepositoryPath);
+               } else {
+                       proxiedRepository = session.getNode(
+                                       RepoConstants.PROXIED_REPOSITORIES).addNode(
+                                       sourceRepo.getId());
+                       proxiedRepository.addMixin(NodeType.MIX_REFERENCEABLE);
+                       JcrUtils.urlToAddressProperties(proxiedRepository,
+                                       sourceRepo.getUrl());
+                       proxiedRepository.setProperty(SLC_URL, sourceRepo.getUrl());
+               }
+
+               origin.setProperty(SLC_PROXY, proxiedRepository);
+               JcrUtils.urlToAddressProperties(origin, remoteUrl);
+       }
+
+       /** Returns the first token of the path */
+       protected String extractRepoId(String path, StringBuilder relativePath) {
+               StringBuilder workspace = new StringBuilder();
+               StringBuilder buf = workspace;
+               for (int i = 1; i < path.length(); i++) {
+                       char c = path.charAt(i);
+                       if (c == '/') {
+                               buf = relativePath;
+                       }
+                       buf.append(c);
+               }
+               return workspace.toString();
+       }
+
+       @Override
+       protected Boolean shouldUpdate(Session clientSession, String nodePath) {
+               // if (nodePath.contains("/repodata/"))
+               // return true;
+               return super.shouldUpdate(clientSession, nodePath);
+       }
+
+       public void setDefaultRepositories(Set<RpmRepository> defaultRepositories) {
+               this.defaultRepositories = defaultRepositories;
+       }
+}
diff --git a/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmSpecFile.java b/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/RpmSpecFile.java
new file mode 100644 (file)
index 0000000..c9132f9
--- /dev/null
@@ -0,0 +1,110 @@
+package org.argeo.slc.rpmfactory.core;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class RpmSpecFile {
+       private Path specFile;
+
+       private String name;
+       private String version;
+       private String release;
+       private Map<String, String> sources = new HashMap<String, String>();
+       private Map<String, String> patches = new HashMap<String, String>();
+
+       public RpmSpecFile(Path specFile) {
+               this.specFile = specFile;
+               parseSpecFile();
+       }
+
+       public void init() {
+               parseSpecFile();
+       }
+
+       protected void parseSpecFile() {
+               try {
+                       List<String> lines = (List<String>) Files.readAllLines(specFile);
+
+                       lines: for (String line : lines) {
+                               int indexSemiColon = line.indexOf(':');
+                               if (indexSemiColon <= 0)
+                                       continue lines;
+                               String directive = line.substring(0, indexSemiColon).trim();
+                               String value = line.substring(indexSemiColon + 1).trim();
+                               if ("name".equals(directive.toLowerCase()))
+                                       name = value;
+                               else if ("version".equals(directive.toLowerCase()))
+                                       version = value;
+                               else if ("release".equals(directive.toLowerCase()))
+                                       release = value;
+                               else if (directive.toLowerCase().startsWith("source"))
+                                       sources.put(directive, interpret(value));
+                               else if (directive.toLowerCase().startsWith("patch"))
+                                       patches.put(directive, interpret(value));
+                       }
+
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot parse spec file " + specFile, e);
+               }
+       }
+
+       protected String interpret(String value) {
+               StringBuffer buf = new StringBuffer(value.length());
+               StringBuffer currKey = null;
+               boolean mayBeKey = false;
+               chars: for (char c : value.toCharArray()) {
+                       if (c == '%')
+                               mayBeKey = true;
+                       else if (c == '{') {
+                               if (mayBeKey)
+                                       currKey = new StringBuffer();
+                       } else if (c == '}') {
+                               if (currKey == null)
+                                       continue chars;
+                               String key = currKey.toString();
+                               if ("name".equals(key.toLowerCase()))
+                                       buf.append(name);
+                               else if ("version".equals(key.toLowerCase()))
+                                       buf.append(version);
+                               else
+                                       buf.append("%{").append(key).append('}');
+                               currKey = null;
+                       } else {
+                               if (currKey != null)
+                                       currKey.append(c);
+                               else
+                                       buf.append(c);
+                       }
+               }
+               return buf.toString();
+       }
+
+       public Path getSpecFile() {
+               return specFile;
+       }
+
+       public String getName() {
+               return name;
+       }
+
+       public String getVersion() {
+               return version;
+       }
+
+       public String getRelease() {
+               return release;
+       }
+
+       public Map<String, String> getSources() {
+               return sources;
+       }
+
+       public Map<String, String> getPatches() {
+               return patches;
+       }
+
+}
diff --git a/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/StagingRpmRepository.java b/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/StagingRpmRepository.java
new file mode 100644 (file)
index 0000000..58977f4
--- /dev/null
@@ -0,0 +1,6 @@
+package org.argeo.slc.rpmfactory.core;
+
+/** Local build repository, used only for builds. */
+public class StagingRpmRepository extends AbstractRpmRepository {
+
+}
diff --git a/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/ThirdPartyRpmRepository.java b/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/ThirdPartyRpmRepository.java
new file mode 100644 (file)
index 0000000..2902530
--- /dev/null
@@ -0,0 +1,18 @@
+package org.argeo.slc.rpmfactory.core;
+
+/**
+ * A repository of third party RPMs used for the build. RPM used by the builds
+ * will be cached within the system.
+ */
+public class ThirdPartyRpmRepository extends AbstractRpmRepository {
+       private String yumConf;
+
+       public String getYumConf() {
+               return yumConf;
+       }
+
+       public void setYumConf(String yumConf) {
+               this.yumConf = yumConf;
+       }
+
+}
diff --git a/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/YumListParser.java b/org.argeo.slc.rpmfactory/src/org/argeo/slc/rpmfactory/core/YumListParser.java
new file mode 100644 (file)
index 0000000..9327fed
--- /dev/null
@@ -0,0 +1,93 @@
+package org.argeo.slc.rpmfactory.core;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.TreeSet;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.LineIterator;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.slc.SlcException;
+
+/**
+ * Reads the output of a 'yum list all' command and interpret the list of
+ * packages.
+ */
+public class YumListParser implements RpmPackageSet {
+       private final static CmsLog log = CmsLog.getLog(YumListParser.class);
+
+       private Set<String> installed = new TreeSet<String>();
+       /** Not installed but available */
+       private Set<String> installable = new TreeSet<String>();
+
+       private Path yumListOutput;
+
+       public void init() {
+               if (yumListOutput != null) {
+                       try (InputStream in = Files.newInputStream(yumListOutput)) {
+                               load(in);
+                               if (log.isDebugEnabled())
+                                       log.debug(installed.size() + " installed, " + installable.size() + " installable, from "
+                                                       + yumListOutput);
+                       } catch (IOException e) {
+                               throw new SlcException("Cannot initialize yum list parser", e);
+                       }
+               }
+       }
+
+       public Boolean contains(String packageName) {
+               if (installed.contains(packageName))
+                       return true;
+               else
+                       return installable.contains(packageName);
+       }
+
+       protected void load(InputStream in) throws IOException {
+               Boolean readingInstalled = false;
+               Boolean readingAvailable = false;
+               LineIterator it = IOUtils.lineIterator(in, "UTF-8");
+               while (it.hasNext()) {
+                       String line = it.nextLine();
+                       if (line.trim().equals("Installed Packages")) {
+                               readingInstalled = true;
+                       } else if (line.trim().equals("Available Packages")) {
+                               readingAvailable = true;
+                               readingInstalled = false;
+                       } else if (readingAvailable) {
+                               if (Character.isLetterOrDigit(line.charAt(0))) {
+                                       installable.add(extractRpmName(line));
+                               }
+                       } else if (readingInstalled) {
+                               if (Character.isLetterOrDigit(line.charAt(0))) {
+                                       installed.add(extractRpmName(line));
+                               }
+                       }
+               }
+       }
+
+       protected String extractRpmName(String line) {
+               StringTokenizer st = new StringTokenizer(line, " \t");
+               String packageName = st.nextToken();
+               // consider the arch as an extension
+               return FilenameUtils.getBaseName(packageName);
+               // return packageName.split("\\.")[0];
+       }
+
+       public Set<String> getInstalled() {
+               return installed;
+       }
+
+       public Set<String> getInstallable() {
+               return installable;
+       }
+
+       public void setYumListOutput(Path yumListOutput) {
+               this.yumListOutput = yumListOutput;
+       }
+
+}
diff --git a/swt/org.argeo.tool.devops.e4/.classpath b/swt/org.argeo.tool.devops.e4/.classpath
new file mode 100644 (file)
index 0000000..81fe078
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/swt/org.argeo.tool.devops.e4/.project b/swt/org.argeo.tool.devops.e4/.project
new file mode 100644 (file)
index 0000000..6a3ee57
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.tool.devops.e4</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ds.core.builder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/swt/org.argeo.tool.devops.e4/OSGI-INF/cmsAdminRap.xml b/swt/org.argeo.tool.devops.e4/OSGI-INF/cmsAdminRap.xml
new file mode 100644 (file)
index 0000000..70f4ae9
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" configuration-policy="optional" name="CMS Admin RAP">
+   <implementation class="org.argeo.cms.jcr.e4.rap.CmsE4AdminApp"/>
+   <service>
+      <provide interface="org.eclipse.rap.rwt.application.ApplicationConfiguration"/>
+      <property name="contextName" type="String" value="slc/tool"/>
+   </service>
+</scr:component>
diff --git a/swt/org.argeo.tool.devops.e4/OSGI-INF/homeRepository.xml b/swt/org.argeo.tool.devops.e4/OSGI-INF/homeRepository.xml
new file mode 100644 (file)
index 0000000..2722aab
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" immediate="true" name="Home Repository">
+   <implementation class="org.argeo.cms.e4.OsgiFilterContextFunction"/>
+   <property name="service.context.key" type="String" value="(cn=ego)"/>
+   <service>
+      <provide interface="org.eclipse.e4.core.contexts.IContextFunction"/>
+   </service>
+</scr:component>
diff --git a/swt/org.argeo.tool.devops.e4/OSGI-INF/userAdminWrapper.xml b/swt/org.argeo.tool.devops.e4/OSGI-INF/userAdminWrapper.xml
new file mode 100644 (file)
index 0000000..22e6956
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" immediate="true" name="User Admin Wrapper">
+   <implementation class="org.argeo.cms.e4.users.UserAdminWrapper"/>
+   <reference bind="setUserTransaction" cardinality="1..1" interface="org.argeo.api.cms.transaction.WorkTransaction" name="UserTransaction" policy="static"/>
+   <reference bind="setUserAdmin" cardinality="1..1" interface="org.osgi.service.useradmin.UserAdmin" name="UserAdmin" policy="static"/>
+   <service>
+      <provide interface="org.argeo.cms.e4.users.UserAdminWrapper"/>
+   </service>
+   <reference bind="addUserDirectory" cardinality="0..n" interface="org.argeo.cms.osgi.useradmin.UserDirectory" name="UserDirectory" policy="static" unbind="removeUserDirectory"/>
+</scr:component>
diff --git a/swt/org.argeo.tool.devops.e4/bnd.bnd b/swt/org.argeo.tool.devops.e4/bnd.bnd
new file mode 100644 (file)
index 0000000..ad554cc
--- /dev/null
@@ -0,0 +1,21 @@
+Bundle-ActivationPolicy: lazy
+
+Import-Package: \
+org.eclipse.swt,\
+org.eclipse.swt.widgets;version="0.0.0",\
+org.eclipse.jface.window,\
+org.eclipse.core.commands.common,\
+org.eclipse.e4.ui.model.application.ui;resolution:=optional,\
+org.eclipse.e4.ui.model.application;resolution:=optional,\
+javax.jcr.nodetype,\
+org.argeo.cms,\
+org.argeo.jcr,\
+org.argeo.api.acr,\
+org.argeo.api.cms.directory,\
+org.argeo.cms.e4.rap;resolution:=optional,\
+org.eclipse.*;resolution:=optional,\
+*
+
+Service-Component: OSGI-INF/homeRepository.xml,\
+OSGI-INF/userAdminWrapper.xml,\
+OSGI-INF/cmsAdminRap.xml,\
diff --git a/swt/org.argeo.tool.devops.e4/build.properties b/swt/org.argeo.tool.devops.e4/build.properties
new file mode 100644 (file)
index 0000000..34d2e4d
--- /dev/null
@@ -0,0 +1,4 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .
diff --git a/swt/org.argeo.tool.devops.e4/e4xmi/devops.e4xmi b/swt/org.argeo.tool.devops.e4/e4xmi/devops.e4xmi
new file mode 100644 (file)
index 0000000..d9361b8
--- /dev/null
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="ASCII"?>
+<application:Application xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:advanced="http://www.eclipse.org/ui/2010/UIModel/application/ui/advanced" xmlns:application="http://www.eclipse.org/ui/2010/UIModel/application" xmlns:basic="http://www.eclipse.org/ui/2010/UIModel/application/ui/basic" xmlns:menu="http://www.eclipse.org/ui/2010/UIModel/application/ui/menu" xmi:id="_XqkCQKknEeObFrG_clJBYA" elementId="">
+  <children xsi:type="basic:TrimmedWindow" xmi:id="_Zdy6cKknEeObFrG_clJBYA" elementId="org.argeo.cms.e4.apps.admin.trimmedwindow.0" label="" x="10" y="10" width="500" height="500">
+    <persistedState key="styleOverride" value="8"/>
+    <tags>shellMaximized</tags>
+    <tags>auth.cn=admin,ou=roles,ou=node</tags>
+    <children xsi:type="advanced:PerspectiveStack" xmi:id="_jXVqsCk4Eein5vuhpK-Dew" elementId="org.argeo.cms.e4.perspectivestack.0" selectedElement="_xOVlsDvOEeiF1foPJZSZkw">
+      <children xsi:type="advanced:Perspective" xmi:id="_xOVlsDvOEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.perspective.users" label="Users" iconURI="platform:/plugin/org.argeo.tool.swt/icons/group.png">
+        <tags>auth.cn=admin,ou=roles,ou=node</tags>
+        <children xsi:type="basic:PartSashContainer" xmi:id="_1tQoEDvOEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.partsashcontainer.2" horizontal="true">
+          <children xsi:type="basic:PartStack" xmi:id="_vtbKkDvkEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.partstack.4" containerData="4000" selectedElement="_9gukYDvOEeiF1foPJZSZkw">
+            <children xsi:type="basic:Part" xmi:id="_9gukYDvOEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.part.users" containerData="" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.users.UsersView" label="Users" iconURI="platform:/plugin/org.argeo.tool.swt/icons/person.png">
+              <handlers xmi:id="_0mN68DvjEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handler.4" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.users.handlers.NewUser" command="_uL5i4DvjEeiF1foPJZSZkw"/>
+              <handlers xmi:id="_ODLdgDvkEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handler.5" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.users.handlers.DeleteUsers" command="_xkcMADvjEeiF1foPJZSZkw"/>
+              <toolbar xmi:id="_jLWmkDvjEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.toolbar.1">
+                <children xsi:type="menu:HandledToolItem" xmi:id="_jy_OUDvjEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handledtoolitem.new" label="New" iconURI="platform:/plugin/org.argeo.tool.swt/icons/add.png" command="_uL5i4DvjEeiF1foPJZSZkw"/>
+                <children xsi:type="menu:HandledToolItem" xmi:id="_9qszMDvjEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handledtoolitem.delete" label="Delete" iconURI="platform:/plugin/org.argeo.tool.swt/icons/delete.png" command="_xkcMADvjEeiF1foPJZSZkw"/>
+              </toolbar>
+            </children>
+          </children>
+          <children xsi:type="basic:PartStack" xmi:id="__g1a8DvOEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.partstack.3" containerData="4000">
+            <tags>usersEditorArea</tags>
+          </children>
+          <children xsi:type="basic:PartStack" xmi:id="_-mFn8DvkEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.partstack.5" containerData="2000">
+            <children xsi:type="basic:Part" xmi:id="_6etk4DvOEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.part.groups" containerData="" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.users.GroupsView" label="Groups" iconURI="platform:/plugin/org.argeo.tool.swt/icons/group.png">
+              <handlers xmi:id="_cmShoDvkEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handler.6" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.users.handlers.NewGroup" command="_uL5i4DvjEeiF1foPJZSZkw"/>
+              <handlers xmi:id="_fbYfcDvkEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handler.7" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.users.handlers.DeleteGroups" command="_xkcMADvjEeiF1foPJZSZkw"/>
+              <toolbar xmi:id="_Us0rADvkEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.toolbar.2">
+                <children xsi:type="menu:HandledToolItem" xmi:id="_VQTLgDvkEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handledtoolitem.new" label="New" iconURI="platform:/plugin/org.argeo.tool.swt/icons/add.png" command="_uL5i4DvjEeiF1foPJZSZkw"/>
+                <children xsi:type="menu:HandledToolItem" xmi:id="_XfME8DvkEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handledtoolitem.delete" label="Delete" iconURI="platform:/plugin/org.argeo.tool.swt/icons/delete.png" command="_xkcMADvjEeiF1foPJZSZkw"/>
+              </toolbar>
+            </children>
+          </children>
+        </children>
+      </children>
+      <children xsi:type="advanced:Perspective" xmi:id="_jvjWYCk4Eein5vuhpK-Dew" elementId="org.argeo.cms.e4.perspective.data" label="Data" iconURI="platform:/plugin/org.argeo.tool.swt/icons/nodes.gif">
+        <children xsi:type="basic:PartSashContainer" xmi:id="_h3tvMCkxEein5vuhpK-Dew" elementId="org.argeo.cms.e4.partsashcontainer.0" selectedElement="_0B9SECkxEein5vuhpK-Dew" horizontal="true">
+          <children xsi:type="basic:PartStack" xmi:id="_0B9SECkxEein5vuhpK-Dew" elementId="org.argeo.cms.e4.partstack.0" containerData="4000" selectedElement="_WAjPkCkTEein5vuhpK-Dew">
+            <children xsi:type="basic:Part" xmi:id="_WAjPkCkTEein5vuhpK-Dew" elementId="org.argeo.cms.e4.jcrbrowser" containerData="" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.jcr.JcrBrowserView" label="JCR" iconURI="platform:/plugin/org.argeo.tool.swt/icons/browser.gif">
+              <menus xsi:type="menu:PopupMenu" xmi:id="_eXiUECqREeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.popupmenu.nodeViewer">
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_GVeO8CqhEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handledmenuitem.refresh" label="Refresh" iconURI="platform:/plugin/org.argeo.tool.swt/icons/refresh.png" command="_TOKHsCqYEeidr6NYQH6GbQ"/>
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_fU238CqREeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handledmenuitem.addfoldernode" label="Add folder" iconURI="platform:/plugin/org.argeo.tool.swt/icons/addFolder.gif" command="_RgE5cCqREeidr6NYQH6GbQ"/>
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_U4o9cCqhEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handledmenuitem.rename" label="Rename" iconURI="platform:/plugin/org.argeo.tool.swt/icons/rename.gif" command="_ZrcUMCqYEeidr6NYQH6GbQ"/>
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_Ncxo0CqhEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handledmenuitem.remove" label="Remove" iconURI="platform:/plugin/org.argeo.tool.swt/icons/remove.gif" command="_ChJ-4CqYEeidr6NYQH6GbQ"/>
+              </menus>
+              <menus xmi:id="_oRg_ACqTEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.menu.0">
+                <tags>ViewMenu</tags>
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_yJR8ECqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handledmenuitem.refresh" label="Refresh" iconURI="platform:/plugin/org.argeo.tool.swt/icons/refresh.png" command="_TOKHsCqYEeidr6NYQH6GbQ"/>
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_o6HQECqTEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handledmenuitem.addfoldernode" label="Add folder" iconURI="platform:/plugin/org.argeo.tool.swt/icons/addFolder.gif" command="_RgE5cCqREeidr6NYQH6GbQ"/>
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_5D7aACqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handledmenuitem.rename" label="Rename" iconURI="platform:/plugin/org.argeo.tool.swt/icons/rename.gif" command="_ZrcUMCqYEeidr6NYQH6GbQ"/>
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_7rR2wCqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handledmenuitem.delete" label="Delete" iconURI="platform:/plugin/org.argeo.tool.swt/icons/remove.gif" command="_ChJ-4CqYEeidr6NYQH6GbQ"/>
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_XsHLgFgQEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.handledmenuitem.0" iconURI="platform:/plugin/org.argeo.tool.swt/icons/addRepo.gif" command="_ZWpasFgQEeiknZQLx-vtnA"/>
+              </menus>
+            </children>
+          </children>
+          <children xsi:type="basic:PartStack" xmi:id="_mHrEUCk4Eein5vuhpK-Dew" elementId="org.argeo.cms.e4.partstack.1" containerData="6000">
+            <tags>dataExplorer</tags>
+          </children>
+        </children>
+      </children>
+      <children xsi:type="advanced:Perspective" xmi:id="_u5ZakFhJEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.perspective.monitoring" label="Monitoring" iconURI="platform:/plugin/org.argeo.tool.swt/icons/bundles.gif">
+        <children xsi:type="basic:PartStack" xmi:id="_7i7t8FhJEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.partstack.6">
+          <children xsi:type="basic:Part" xmi:id="_Z-3cMFhbEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.part.osgiConfigurations" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.monitoring.OsgiConfigurationsView" label="OSGi Configurations" iconURI="platform:/plugin/org.argeo.tool.swt/icons/node.gif"/>
+          <children xsi:type="basic:Part" xmi:id="_8dM90FhJEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.part.cmsSessions" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.monitoring.CmsSessionsView" label="CMS Sessions" iconURI="platform:/plugin/org.argeo.tool.swt/icons/person-logged-in.png"/>
+          <children xsi:type="basic:Part" xmi:id="_KqRZIFhNEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.part.modules" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.monitoring.ModulesView" label="Modules" iconURI="platform:/plugin/org.argeo.tool.swt/icons/bundles.gif"/>
+          <children xsi:type="basic:Part" xmi:id="_dXtIoFhNEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.part.bundles" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.monitoring.BundlesView" label="Bundles" iconURI="platform:/plugin/org.argeo.tool.swt/icons/bundles.gif"/>
+        </children>
+      </children>
+      <children xsi:type="advanced:Perspective" xmi:id="_ABK2ADsNEeiUntFYWh-hFg" elementId="org.argeo.cms.e4.perspective.files" label="Files" iconURI="platform:/plugin/org.argeo.tool.swt/icons/file.gif">
+        <children xsi:type="basic:PartSashContainer" xmi:id="_FPimEDsSEeiUntFYWh-hFg" elementId="org.argeo.cms.e4.partsashcontainer.1" horizontal="true">
+          <children xsi:type="basic:PartStack" xmi:id="_H93NgDsSEeiUntFYWh-hFg" elementId="org.argeo.cms.e4.partstack.2" containerData="4000">
+            <children xsi:type="basic:Part" xmi:id="_Izxh0DsSEeiUntFYWh-hFg" elementId="org.argeo.cms.e4.part.files" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.files.NodeFsBrowserView" label="Files" iconURI="platform:/plugin/org.argeo.tool.swt/icons/file.gif"/>
+          </children>
+          <children xsi:type="basic:Part" xmi:id="_TMqBMDsSEeiUntFYWh-hFg" elementId="org.argeo.cms.e4.part.0" containerData="6000"/>
+        </children>
+      </children>
+    </children>
+    <handlers xmi:id="_Vwax0DvrEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handler.8" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.handlers.OpenPerspective" command="_AF1UsDvrEeiF1foPJZSZkw"/>
+    <trimBars xmi:id="_euVxMCk2Eein5vuhpK-Dew" elementId="org.argeo.cms.e4.trimbar.0" side="Left">
+      <children xsi:type="menu:ToolBar" xmi:id="_fotHsCk2Eein5vuhpK-Dew" elementId="org.argeo.cms.e4.toolbar.0">
+        <children xsi:type="menu:HandledToolItem" xmi:id="_jCSQgDvrEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handledtoolitem.users" label="Users" iconURI="platform:/plugin/org.argeo.tool.swt/icons/group.png" command="_AF1UsDvrEeiF1foPJZSZkw">
+          <tags>auth.cn=admin,ou=roles,ou=node</tags>
+          <parameters xmi:id="_lu_uYDvrEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.parameter.2" name="perspectiveId" value="org.argeo.cms.e4.perspective.users"/>
+        </children>
+        <children xsi:type="menu:HandledToolItem" xmi:id="_jfUM4Ck2Eein5vuhpK-Dew" elementId="org.argeo.cms.e4.handledtoolitem.test" label="Data" iconURI="platform:/plugin/org.argeo.tool.swt/icons/nodes.gif" command="_AF1UsDvrEeiF1foPJZSZkw">
+          <parameters xmi:id="_KDlXQDvrEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.parameter.0" name="perspectiveId" value="org.argeo.cms.e4.perspective.data"/>
+        </children>
+        <children xsi:type="menu:HandledToolItem" xmi:id="_dhv80FhKEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.handledtoolitem.monitoring" label="Monitoring" iconURI="platform:/plugin/org.argeo.tool.swt/icons/bundles.gif" command="_AF1UsDvrEeiF1foPJZSZkw">
+          <parameters xmi:id="_kjN0cFhKEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.parameter.3" name="perspectiveId" value="org.argeo.cms.e4.perspective.monitoring"/>
+        </children>
+        <children xsi:type="menu:HandledToolItem" xmi:id="_b0OHUDvrEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handledtoolitem.files" label="Files" iconURI="platform:/plugin/org.argeo.tool.swt/icons/file.gif" command="_AF1UsDvrEeiF1foPJZSZkw">
+          <parameters xmi:id="_fXvRYDvrEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.parameter.1" name="perspectiveId" value="org.argeo.cms.e4.perspective.files"/>
+        </children>
+        <children xsi:type="menu:ToolBarSeparator" xmi:id="_wuoL8FhLEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.toolbarseparator.0"/>
+        <children xsi:type="menu:HandledToolItem" xmi:id="_2v8DkFhKEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.handledtoolitem.logout" label="Log out" iconURI="platform:/plugin/org.argeo.tool.swt/icons/logout.png" command="_PsWd0FhLEeiknZQLx-vtnA"/>
+      </children>
+    </trimBars>
+  </children>
+  <handlers xmi:id="_Xp-P4CqREeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handler.0" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.jcr.handlers.AddFolderNode" command="_RgE5cCqREeidr6NYQH6GbQ"/>
+  <handlers xmi:id="_jbnNwCqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handler.1" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.jcr.handlers.DeleteNodes" command="_ChJ-4CqYEeidr6NYQH6GbQ"/>
+  <handlers xmi:id="_loxB0CqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handler.2" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.jcr.handlers.Refresh" command="_TOKHsCqYEeidr6NYQH6GbQ"/>
+  <handlers xmi:id="_omPfkCqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handler.3" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.jcr.handlers.RenameNode" command="_ZrcUMCqYEeidr6NYQH6GbQ"/>
+  <handlers xmi:id="_dUg-cFgQEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.handler.9" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.jcr.handlers.AddRemoteRepository" command="_ZWpasFgQEeiknZQLx-vtnA"/>
+  <handlers xmi:id="_RQyFAFhLEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.handler.10" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.handlers.CloseWorkbench" command="_PsWd0FhLEeiknZQLx-vtnA"/>
+  <descriptors xmi:id="_XzfoMCqlEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.partdescriptor.nodeEditor" label="Node Editor" iconURI="platform:/plugin/org.argeo.tool.swt/icons/node.gif" allowMultiple="true" category="dataExplorer" closeable="true" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.jcr.JcrNodeEditor"/>
+  <descriptors xmi:id="_sAdNwDvdEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.partdescriptor.userEditor" label="User Editor" iconURI="platform:/plugin/org.argeo.tool.swt/icons/person.png" allowMultiple="true" category="usersEditorArea" closeable="true" dirtyable="true" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.users.UserEditor"/>
+  <descriptors xmi:id="_5nK7EDvdEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.partdescriptor.groupEditor" label="Group Editor" iconURI="platform:/plugin/org.argeo.tool.swt/icons/group.png" allowMultiple="true" category="usersEditorArea" closeable="true" dirtyable="true" contributionURI="bundleclass://org.argeo.tool.devops.e4/org.argeo.cms.e4.users.GroupEditor"/>
+  <commands xmi:id="_RgE5cCqREeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.command.addFolderNode" commandName="Add folder node" category="_MDkwUCqYEeidr6NYQH6GbQ"/>
+  <commands xmi:id="_ChJ-4CqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.command.deleteNodes" commandName="Delete nodes" category="_MDkwUCqYEeidr6NYQH6GbQ"/>
+  <commands xmi:id="_TOKHsCqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.command.refreshNodes" commandName="Refresh nodes" category="_MDkwUCqYEeidr6NYQH6GbQ"/>
+  <commands xmi:id="_ZrcUMCqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.command.renameNode" commandName="Rename node" category="_MDkwUCqYEeidr6NYQH6GbQ"/>
+  <commands xmi:id="_uL5i4DvjEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.command.add" commandName="Add"/>
+  <commands xmi:id="_xkcMADvjEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.command.delete" commandName="Delete"/>
+  <commands xmi:id="_AF1UsDvrEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.command.openPerspective" commandName="Open Perspective">
+    <parameters xmi:id="_F3WAUDvrEeiF1foPJZSZkw" elementId="perspectiveId" name="Perspective Id" optional="false"/>
+  </commands>
+  <commands xmi:id="_ZWpasFgQEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.command.addRemoteRepository" commandName="Add Remote Repository"/>
+  <commands xmi:id="_PsWd0FhLEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.command.logout" commandName="Log out"/>
+  <addons xmi:id="_XqkCQaknEeObFrG_clJBYA" elementId="org.eclipse.e4.core.commands.service" contributionURI="bundleclass://org.eclipse.e4.core.commands/org.eclipse.e4.core.commands.CommandServiceAddon"/>
+  <addons xmi:id="_XqkCQqknEeObFrG_clJBYA" elementId="org.eclipse.e4.ui.contexts.service" contributionURI="bundleclass://org.eclipse.e4.ui.services/org.eclipse.e4.ui.services.ContextServiceAddon"/>
+  <addons xmi:id="_XqkCQ6knEeObFrG_clJBYA" elementId="org.eclipse.e4.ui.bindings.service" contributionURI="bundleclass://org.eclipse.e4.ui.bindings/org.eclipse.e4.ui.bindings.BindingServiceAddon"/>
+  <addons xmi:id="_XqkCRKknEeObFrG_clJBYA" elementId="org.eclipse.e4.ui.workbench.commands.model" contributionURI="bundleclass://org.eclipse.e4.ui.workbench/org.eclipse.e4.ui.internal.workbench.addons.CommandProcessingAddon"/>
+  <addons xmi:id="_XqkCRaknEeObFrG_clJBYA" elementId="org.eclipse.e4.ui.workbench.contexts.model" contributionURI="bundleclass://org.eclipse.e4.ui.workbench/org.eclipse.e4.ui.internal.workbench.addons.ContextProcessingAddon"/>
+  <addons xmi:id="_XqkCRqknEeObFrG_clJBYA" elementId="org.eclipse.e4.ui.workbench.bindings.model" contributionURI="bundleclass://org.eclipse.e4.ui.workbench.swt/org.eclipse.e4.ui.workbench.swt.util.BindingProcessingAddon"/>
+  <addons xmi:id="_XqkCR6knEeObFrG_clJBYA" elementId="org.eclipse.e4.ui.workbench.handler.model" contributionURI="bundleclass://org.eclipse.e4.ui.workbench/org.eclipse.e4.ui.internal.workbench.addons.HandlerProcessingAddon"/>
+  <addons xmi:id="_8VnK8OdKEeijEOqYKRSeoQ" elementId="org.argeo.cms.e4.addon.locale" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.addons.LocaleAddon"/>
+  <addons xmi:id="_-xeJYOdKEeijEOqYKRSeoQ" elementId="org.argeo.cms.e4.addon.auth" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.addons.AuthAddon"/>
+  <categories xmi:id="_MDkwUCqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.category.jcrBrowser" name="JCR Browser"/>
+</application:Application>
\ No newline at end of file
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/files/NodeFsBrowserView.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/files/NodeFsBrowserView.java
new file mode 100644 (file)
index 0000000..aabfbf5
--- /dev/null
@@ -0,0 +1,47 @@
+package org.argeo.cms.e4.files;
+
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.spi.FileSystemProvider;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+
+import org.argeo.eclipse.ui.fs.SimpleFsBrowser;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+
+/** Browse the node file system. */
+public class NodeFsBrowserView {
+       // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID +
+       // ".nodeFsBrowserView";
+
+       @Inject
+       FileSystemProvider nodeFileSystemProvider;
+
+       @PostConstruct
+       public void createPartControl(Composite parent) {
+               try {
+                       // URI uri = new URI("node://root:demo@localhost:7070/");
+                       URI uri = new URI("node:///");
+                       FileSystem fileSystem = nodeFileSystemProvider.getFileSystem(uri);
+                       if (fileSystem == null)
+                               fileSystem = nodeFileSystemProvider.newFileSystem(uri, null);
+                       Path nodePath = fileSystem.getPath("/");
+
+                       Path localPath = Paths.get(System.getProperty("user.home"));
+
+                       SimpleFsBrowser browser = new SimpleFsBrowser(parent, SWT.NO_FOCUS);
+                       browser.setInput(nodePath, localPath);
+//                     AdvancedFsBrowser browser = new AdvancedFsBrowser();
+//                     browser.createUi(parent, localPath);
+               } catch (Exception e) {
+                       throw new RuntimeException("Cannot open file system browser", e);
+               }
+       }
+
+       public void setFocus() {
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/files/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/files/package-info.java
new file mode 100644 (file)
index 0000000..b481dd4
--- /dev/null
@@ -0,0 +1,2 @@
+/** Files browser perspective. */
+package org.argeo.cms.e4.files;
\ No newline at end of file
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/EclipseJcrMonitor.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/EclipseJcrMonitor.java
new file mode 100644 (file)
index 0000000..e10738e
--- /dev/null
@@ -0,0 +1,44 @@
+package org.argeo.cms.e4.jcr;
+
+import org.argeo.jcr.JcrMonitor;
+import org.eclipse.core.runtime.IProgressMonitor;
+
+/**
+ * Wraps an Eclipse {@link IProgressMonitor} so that it can be passed to
+ * framework agnostic Argeo routines.
+ */
+public class EclipseJcrMonitor implements JcrMonitor {
+       private final IProgressMonitor progressMonitor;
+
+       public EclipseJcrMonitor(IProgressMonitor progressMonitor) {
+               this.progressMonitor = progressMonitor;
+       }
+
+       public void beginTask(String name, int totalWork) {
+               progressMonitor.beginTask(name, totalWork);
+       }
+
+       public void done() {
+               progressMonitor.done();
+       }
+
+       public boolean isCanceled() {
+               return progressMonitor.isCanceled();
+       }
+
+       public void setCanceled(boolean value) {
+               progressMonitor.setCanceled(value);
+       }
+
+       public void setTaskName(String name) {
+               progressMonitor.setTaskName(name);
+       }
+
+       public void subTask(String name) {
+               progressMonitor.subTask(name);
+       }
+
+       public void worked(int work) {
+               progressMonitor.worked(work);
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/GenericPropertyPage.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/GenericPropertyPage.java
new file mode 100644 (file)
index 0000000..e17f17b
--- /dev/null
@@ -0,0 +1,141 @@
+package org.argeo.cms.e4.jcr;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.ui.jcr.PropertyLabelProvider;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.layout.TreeColumnLayout;
+import org.eclipse.jface.viewers.ColumnWeightData;
+import org.eclipse.jface.viewers.IBaseLabelProvider;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+
+/**
+ * Generic editor property page. Lists all properties of current node as a
+ * complex tree. TODO: enable editing
+ */
+public class GenericPropertyPage {
+
+       // Main business Objects
+       private Node currentNode;
+
+       public GenericPropertyPage(Node currentNode) {
+               this.currentNode = currentNode;
+       }
+
+       protected void createFormContent(Composite parent) {
+               Composite innerBox = new Composite(parent, SWT.NONE);
+               // Composite innerBox = new Composite(body, SWT.NO_FOCUS);
+               FillLayout layout = new FillLayout();
+               layout.marginHeight = 5;
+               layout.marginWidth = 5;
+               innerBox.setLayout(layout);
+               createComplexTree(innerBox);
+               // TODO TreeColumnLayout triggers a scroll issue with the form:
+               // The inside body is always to big and a scroll bar is shown
+               // Composite tableCmp = new Composite(body, SWT.NO_FOCUS);
+               // createComplexTree(tableCmp);
+       }
+
+       private TreeViewer createComplexTree(Composite parent) {
+               int style = SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION;
+               Tree tree = new Tree(parent, style);
+               TreeColumnLayout tableColumnLayout = new TreeColumnLayout();
+
+               createColumn(tree, tableColumnLayout, "Property", SWT.LEFT, 200, 30);
+               createColumn(tree, tableColumnLayout, "Value(s)", SWT.LEFT, 300, 60);
+               createColumn(tree, tableColumnLayout, "Type", SWT.LEFT, 75, 10);
+               createColumn(tree, tableColumnLayout, "Attributes", SWT.LEFT, 75, 0);
+               // Do not apply the treeColumnLayout it does not work yet
+               // parent.setLayout(tableColumnLayout);
+
+               tree.setLinesVisible(true);
+               tree.setHeaderVisible(true);
+
+               TreeViewer treeViewer = new TreeViewer(tree);
+               treeViewer.setContentProvider(new TreeContentProvider());
+               treeViewer.setLabelProvider((IBaseLabelProvider) new PropertyLabelProvider());
+               treeViewer.setInput(currentNode);
+               treeViewer.expandAll();
+               return treeViewer;
+       }
+
+       private static TreeColumn createColumn(Tree parent, TreeColumnLayout tableColumnLayout, String name, int style,
+                       int width, int weight) {
+               TreeColumn column = new TreeColumn(parent, style);
+               column.setText(name);
+               column.setWidth(width);
+               column.setMoveable(true);
+               column.setResizable(true);
+               tableColumnLayout.setColumnData(column, new ColumnWeightData(weight, width, true));
+               return column;
+       }
+
+       private class TreeContentProvider implements ITreeContentProvider {
+               private static final long serialVersionUID = -6162736530019406214L;
+
+               public Object[] getElements(Object parent) {
+                       Object[] props = null;
+                       try {
+
+                               if (parent instanceof Node) {
+                                       Node node = (Node) parent;
+                                       PropertyIterator pi;
+                                       pi = node.getProperties();
+                                       List<Property> propList = new ArrayList<Property>();
+                                       while (pi.hasNext()) {
+                                               propList.add(pi.nextProperty());
+                                       }
+                                       props = propList.toArray();
+                               }
+                       } catch (RepositoryException e) {
+                               throw new EclipseUiException("Unexpected exception while listing node properties", e);
+                       }
+                       return props;
+               }
+
+               public Object getParent(Object child) {
+                       return null;
+               }
+
+               public Object[] getChildren(Object parent) {
+                       if (parent instanceof Property) {
+                               Property prop = (Property) parent;
+                               try {
+                                       if (prop.isMultiple())
+                                               return prop.getValues();
+                               } catch (RepositoryException e) {
+                                       throw new EclipseUiException("Cannot get multi-prop values on " + prop, e);
+                               }
+                       }
+                       return null;
+               }
+
+               public boolean hasChildren(Object parent) {
+                       try {
+                               return (parent instanceof Property && ((Property) parent).isMultiple());
+                       } catch (RepositoryException e) {
+                               throw new EclipseUiException("Cannot check if property is multiple for " + parent, e);
+                       }
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+
+               public void dispose() {
+               }
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrBrowserView.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrBrowserView.java
new file mode 100644 (file)
index 0000000..dc0ba94
--- /dev/null
@@ -0,0 +1,349 @@
+package org.argeo.cms.e4.jcr;
+
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventListener;
+import javax.jcr.observation.ObservationManager;
+
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.keyring.CryptoKeyring;
+import org.argeo.api.cms.keyring.Keyring;
+import org.argeo.cms.swt.CmsException;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.ui.jcr.JcrBrowserUtils;
+import org.argeo.cms.ui.jcr.NodeContentProvider;
+import org.argeo.cms.ui.jcr.NodeLabelProvider;
+import org.argeo.cms.ui.jcr.OsgiRepositoryRegister;
+import org.argeo.cms.ui.jcr.PropertiesContentProvider;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ux.widgets.TreeParent;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.jcr.AsyncUiEventListener;
+import org.argeo.eclipse.ui.jcr.util.NodeViewerComparer;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.e4.core.contexts.IEclipseContext;
+import org.eclipse.e4.core.di.annotations.Optional;
+import org.eclipse.e4.ui.services.EMenuService;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.IBaseLabelProvider;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+
+/**
+ * Basic View to display a sash form to browse a JCR compliant multiple
+ * repository environment
+ */
+public class JcrBrowserView {
+       final static String ID = "org.argeo.cms.e4.jcrbrowser";
+       final static String NODE_VIEWER_POPUP_MENU_ID = "org.argeo.cms.e4.popupmenu.nodeViewer";
+
+       private boolean sortChildNodes = true;
+
+       /* DEPENDENCY INJECTION */
+       @Inject
+       @Optional
+       private Keyring keyring;
+       @Inject
+       private RepositoryFactory repositoryFactory;
+       @Inject
+       private Repository nodeRepository;
+
+       // Current user session on the home repository default workspace
+       private Session userSession;
+
+       private OsgiRepositoryRegister repositoryRegister = new OsgiRepositoryRegister();
+
+       // This page widgets
+       private TreeViewer nodesViewer;
+       private NodeContentProvider nodeContentProvider;
+       private TableViewer propertiesViewer;
+       private EventListener resultsObserver;
+
+       @PostConstruct
+       public void createPartControl(Composite parent, IEclipseContext context, EPartService partService,
+                       ESelectionService selectionService, EMenuService menuService) {
+               repositoryRegister.init();
+
+               parent.setLayout(new FillLayout());
+               SashForm sashForm = new SashForm(parent, SWT.VERTICAL);
+               // sashForm.setSashWidth(4);
+               // sashForm.setLayout(new FillLayout());
+
+               // Create the tree on top of the view
+               Composite top = new Composite(sashForm, SWT.NONE);
+               // GridLayout gl = new GridLayout(1, false);
+               top.setLayout(CmsSwtUtils.noSpaceGridLayout());
+
+               try {
+                       this.userSession = this.nodeRepository.login(CmsConstants.HOME_WORKSPACE);
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot open user session", e);
+               }
+
+               nodeContentProvider = new NodeContentProvider(userSession, keyring, repositoryRegister, repositoryFactory,
+                               sortChildNodes);
+
+               // nodes viewer
+               nodesViewer = createNodeViewer(top, nodeContentProvider);
+
+               // context menu : it is completely defined in the plugin.xml file.
+               // MenuManager menuManager = new MenuManager();
+               // Menu menu = menuManager.createContextMenu(nodesViewer.getTree());
+
+               // nodesViewer.getTree().setMenu(menu);
+
+               nodesViewer.setInput("");
+
+               // Create the property viewer on the bottom
+               Composite bottom = new Composite(sashForm, SWT.NONE);
+               bottom.setLayout(CmsSwtUtils.noSpaceGridLayout());
+               propertiesViewer = createPropertiesViewer(bottom);
+
+               sashForm.setWeights(getWeights());
+               nodesViewer.setComparer(new NodeViewerComparer());
+               nodesViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+                       public void selectionChanged(SelectionChangedEvent event) {
+                               IStructuredSelection selection = (IStructuredSelection) event.getSelection();
+                               selectionService.setSelection(selection.toList());
+                       }
+               });
+               nodesViewer.addDoubleClickListener(new JcrE4DClickListener(nodesViewer, partService));
+               menuService.registerContextMenu(nodesViewer.getControl(), NODE_VIEWER_POPUP_MENU_ID);
+               // getSite().registerContextMenu(menuManager, nodesViewer);
+               // getSite().setSelectionProvider(nodesViewer);
+       }
+
+       @PreDestroy
+       public void dispose() {
+               JcrUtils.logoutQuietly(userSession);
+               repositoryRegister.destroy();
+       }
+
+       public void refresh(Object obj) {
+               // Enable full refresh from a command when no element of the tree is
+               // selected
+               if (obj == null) {
+                       Object[] elements = nodeContentProvider.getElements(null);
+                       for (Object el : elements) {
+                               if (el instanceof TreeParent)
+                                       JcrBrowserUtils.forceRefreshIfNeeded((TreeParent) el);
+                               getNodeViewer().refresh(el);
+                       }
+               } else
+                       getNodeViewer().refresh(obj);
+       }
+
+       /**
+        * To be overridden to adapt size of form and result frames.
+        */
+       protected int[] getWeights() {
+               return new int[] { 70, 30 };
+       }
+
+       protected TreeViewer createNodeViewer(Composite parent, final ITreeContentProvider nodeContentProvider) {
+
+               final TreeViewer tmpNodeViewer = new TreeViewer(parent, SWT.MULTI);
+
+               tmpNodeViewer.getTree().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               tmpNodeViewer.setContentProvider(nodeContentProvider);
+               tmpNodeViewer.setLabelProvider((IBaseLabelProvider) new NodeLabelProvider());
+               tmpNodeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+                       public void selectionChanged(SelectionChangedEvent event) {
+                               if (!event.getSelection().isEmpty()) {
+                                       IStructuredSelection sel = (IStructuredSelection) event.getSelection();
+                                       Object firstItem = sel.getFirstElement();
+                                       if (firstItem instanceof SingleJcrNodeElem)
+                                               propertiesViewer.setInput(((SingleJcrNodeElem) firstItem).getNode());
+                               } else {
+                                       propertiesViewer.setInput("");
+                               }
+                       }
+               });
+
+               resultsObserver = new TreeObserver(tmpNodeViewer.getTree().getDisplay());
+               if (keyring != null)
+                       try {
+                               ObservationManager observationManager = userSession.getWorkspace().getObservationManager();
+                               observationManager.addEventListener(resultsObserver, Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED, "/",
+                                               true, null, null, false);
+                       } catch (RepositoryException e) {
+                               throw new EclipseUiException("Cannot register listeners", e);
+                       }
+
+               // tmpNodeViewer.addDoubleClickListener(new JcrDClickListener(tmpNodeViewer));
+               return tmpNodeViewer;
+       }
+
+       protected TableViewer createPropertiesViewer(Composite parent) {
+               propertiesViewer = new TableViewer(parent, SWT.NONE);
+               propertiesViewer.getTable().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               propertiesViewer.getTable().setHeaderVisible(true);
+               propertiesViewer.setContentProvider(new PropertiesContentProvider());
+               TableViewerColumn col = new TableViewerColumn(propertiesViewer, SWT.NONE);
+               col.getColumn().setText("Name");
+               col.getColumn().setWidth(200);
+               col.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -6684361063107478595L;
+
+                       public String getText(Object element) {
+                               try {
+                                       return ((Property) element).getName();
+                               } catch (RepositoryException e) {
+                                       throw new EclipseUiException("Unexpected exception in label provider", e);
+                               }
+                       }
+               });
+               col = new TableViewerColumn(propertiesViewer, SWT.NONE);
+               col.getColumn().setText("Value");
+               col.getColumn().setWidth(400);
+               col.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -8201994187693336657L;
+
+                       public String getText(Object element) {
+                               try {
+                                       Property property = (Property) element;
+                                       if (property.getType() == PropertyType.BINARY)
+                                               return "<binary>";
+                                       else if (property.isMultiple()) {
+                                               StringBuffer buf = new StringBuffer("[");
+                                               Value[] values = property.getValues();
+                                               for (int i = 0; i < values.length; i++) {
+                                                       if (i != 0)
+                                                               buf.append(", ");
+                                                       buf.append(values[i].getString());
+                                               }
+                                               buf.append(']');
+                                               return buf.toString();
+                                       } else
+                                               return property.getValue().getString();
+                               } catch (RepositoryException e) {
+                                       throw new EclipseUiException("Unexpected exception in label provider", e);
+                               }
+                       }
+               });
+               col = new TableViewerColumn(propertiesViewer, SWT.NONE);
+               col.getColumn().setText("Type");
+               col.getColumn().setWidth(200);
+               col.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -6009599998150286070L;
+
+                       public String getText(Object element) {
+                               return JcrBrowserUtils.getPropertyTypeAsString((Property) element);
+                       }
+               });
+               propertiesViewer.setInput("");
+               return propertiesViewer;
+       }
+
+       protected TreeViewer getNodeViewer() {
+               return nodesViewer;
+       }
+
+       /**
+        * Resets the tree content provider
+        * 
+        * @param sortChildNodes if true the content provider will use a comparer to
+        *                       sort nodes that might slow down the display
+        */
+       public void setSortChildNodes(boolean sortChildNodes) {
+               this.sortChildNodes = sortChildNodes;
+               ((NodeContentProvider) nodesViewer.getContentProvider()).setSortChildren(sortChildNodes);
+               nodesViewer.setInput("");
+       }
+
+       /** Notifies the current view that a node has been added */
+       public void nodeAdded(TreeParent parentNode) {
+               // insure that Ui objects have been correctly created:
+               JcrBrowserUtils.forceRefreshIfNeeded(parentNode);
+               getNodeViewer().refresh(parentNode);
+               getNodeViewer().expandToLevel(parentNode, 1);
+       }
+
+       /** Notifies the current view that a node has been removed */
+       public void nodeRemoved(TreeParent parentNode) {
+               IStructuredSelection newSel = new StructuredSelection(parentNode);
+               getNodeViewer().setSelection(newSel, true);
+               // Force refresh
+               IStructuredSelection tmpSel = (IStructuredSelection) getNodeViewer().getSelection();
+               getNodeViewer().refresh(tmpSel.getFirstElement());
+       }
+
+       class TreeObserver extends AsyncUiEventListener {
+
+               public TreeObserver(Display display) {
+                       super(display);
+               }
+
+               @Override
+               protected Boolean willProcessInUiThread(List<Event> events) throws RepositoryException {
+                       for (Event event : events) {
+                               if (getLog().isTraceEnabled())
+                                       getLog().debug("Received event " + event);
+                               String path = event.getPath();
+                               int index = path.lastIndexOf('/');
+                               String propertyName = path.substring(index + 1);
+                               if (getLog().isTraceEnabled())
+                                       getLog().debug("Concerned property " + propertyName);
+                       }
+                       return false;
+               }
+
+               protected void onEventInUiThread(List<Event> events) throws RepositoryException {
+                       if (getLog().isTraceEnabled())
+                               getLog().trace("Refresh result list");
+                       nodesViewer.refresh();
+               }
+
+       }
+
+       public boolean getSortChildNodes() {
+               return sortChildNodes;
+       }
+
+       public void setFocus() {
+               getNodeViewer().getTree().setFocus();
+       }
+
+       /* DEPENDENCY INJECTION */
+       // public void setRepositoryRegister(RepositoryRegister repositoryRegister) {
+       // this.repositoryRegister = repositoryRegister;
+       // }
+
+       public void setKeyring(CryptoKeyring keyring) {
+               this.keyring = keyring;
+       }
+
+       public void setRepositoryFactory(RepositoryFactory repositoryFactory) {
+               this.repositoryFactory = repositoryFactory;
+       }
+
+       public void setNodeRepository(Repository nodeRepository) {
+               this.nodeRepository = nodeRepository;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrE4DClickListener.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrE4DClickListener.java
new file mode 100644 (file)
index 0000000..f4ee2e8
--- /dev/null
@@ -0,0 +1,36 @@
+package org.argeo.cms.e4.jcr;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.swt.CmsException;
+import org.argeo.cms.ui.jcr.JcrDClickListener;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.e4.ui.workbench.modeling.EPartService.PartState;
+import org.eclipse.jface.viewers.TreeViewer;
+
+public class JcrE4DClickListener extends JcrDClickListener {
+       EPartService partService;
+
+       public JcrE4DClickListener(TreeViewer nodeViewer, EPartService partService) {
+               super(nodeViewer);
+               this.partService = partService;
+       }
+
+       @Override
+       protected void openNode(Node node) {
+               MPart part = partService.createPart(JcrNodeEditor.DESCRIPTOR_ID);
+               try {
+                       part.setLabel(node.getName());
+                       part.getPersistedState().put("nodeWorkspace", node.getSession().getWorkspace().getName());
+                       part.getPersistedState().put("nodePath", node.getPath());
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot open " + node, e);
+               }
+
+               // the provided part is be shown
+               partService.showPart(part, PartState.ACTIVATE);
+       }
+
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrNodeEditor.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrNodeEditor.java
new file mode 100644 (file)
index 0000000..ae2b325
--- /dev/null
@@ -0,0 +1,26 @@
+package org.argeo.cms.e4.jcr;
+
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+import javax.jcr.Node;
+
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+
+public class JcrNodeEditor {
+       final static String DESCRIPTOR_ID = "org.argeo.cms.e4.partdescriptor.nodeEditor";
+
+       @PostConstruct
+       public void createUi(Composite parent, MPart part, ESelectionService selectionService) {
+               parent.setLayout(new FillLayout());
+               List<?> selection = (List<?>) selectionService.getSelection();
+               Node node = ((SingleJcrNodeElem) selection.get(0)).getNode();
+               GenericPropertyPage propertyPage = new GenericPropertyPage(node);
+               propertyPage.createFormContent(parent);
+       }
+
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/SimplePart.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/SimplePart.java
new file mode 100644 (file)
index 0000000..17d8d2a
--- /dev/null
@@ -0,0 +1,19 @@
+package org.argeo.cms.e4.jcr;
+
+import javax.annotation.PostConstruct;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+
+public class SimplePart {
+
+       @PostConstruct
+       void init(Composite parent) {
+               parent.setLayout(new GridLayout());
+               Label label = new Label(parent, SWT.NONE);
+               label.setText("Hello e4 World");
+       }
+
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/AddFolderNode.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/AddFolderNode.java
new file mode 100644 (file)
index 0000000..09fa760
--- /dev/null
@@ -0,0 +1,67 @@
+package org.argeo.cms.e4.jcr.handlers;
+
+import java.util.List;
+
+import javax.inject.Named;
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.cms.e4.jcr.JcrBrowserView;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.jcr.model.WorkspaceElem;
+import org.argeo.cms.ux.widgets.TreeParent;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.argeo.eclipse.ui.dialogs.SingleValue;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.services.IServiceConstants;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+
+/**
+ * Adds a node of type nt:folder, only on {@link SingleJcrNodeElem} and
+ * {@link WorkspaceElem} TreeObject types.
+ * 
+ * This handler assumes that a selection provider is available and picks only
+ * first selected item. It is UI's job to enable the command only when the
+ * selection contains one and only one element. Thus no parameter is passed
+ * through the command.
+ */
+public class AddFolderNode {
+       @Execute
+       public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) {
+               List<?> selection = (List<?>) selectionService.getSelection();
+               JcrBrowserView view = (JcrBrowserView) part.getObject();
+
+               if (selection != null && selection.size() == 1) {
+                       TreeParent treeParentNode = null;
+                       Node jcrParentNode = null;
+                       Object obj = selection.get(0);
+
+                       if (obj instanceof SingleJcrNodeElem) {
+                               treeParentNode = (TreeParent) obj;
+                               jcrParentNode = ((SingleJcrNodeElem) treeParentNode).getNode();
+                       } else if (obj instanceof WorkspaceElem) {
+                               treeParentNode = (TreeParent) obj;
+                               jcrParentNode = ((WorkspaceElem) treeParentNode).getRootNode();
+                       } else
+                               return;
+
+                       String folderName = SingleValue.ask("Folder name", "Enter folder name");
+                       if (folderName != null) {
+                               try {
+                                       jcrParentNode.addNode(folderName, NodeType.NT_FOLDER);
+                                       jcrParentNode.getSession().save();
+                                       view.nodeAdded(treeParentNode);
+                               } catch (RepositoryException e) {
+                                       ErrorFeedback.show("Cannot create folder " + folderName + " under " + treeParentNode, e);
+                               }
+                       }
+               } else {
+                       // ErrorFeedback.show(WorkbenchUiPlugin
+                       // .getMessage("errorUnvalidNtFolderNodeType"));
+                       ErrorFeedback.show("Invalid NT folder node type");
+               }
+       }
+
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/AddRemoteRepository.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/AddRemoteRepository.java
new file mode 100644 (file)
index 0000000..8b8d8b7
--- /dev/null
@@ -0,0 +1,210 @@
+package org.argeo.cms.e4.jcr.handlers;
+
+import java.net.URI;
+import java.util.Hashtable;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.keyring.Keyring;
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.ArgeoTypes;
+import org.argeo.cms.e4.jcr.JcrBrowserView;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.core.di.annotations.Optional;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.services.IServiceConstants;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.TitleAreaDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Connect to a remote repository and, if successful publish it as an OSGi
+ * service.
+ */
+public class AddRemoteRepository {
+
+       @Inject
+       private RepositoryFactory repositoryFactory;
+       @Inject
+       private Repository nodeRepository;
+       @Inject
+       @Optional
+       private Keyring keyring;
+
+       @Execute
+       public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part) {
+               JcrBrowserView view = (JcrBrowserView) part.getObject();
+               RemoteRepositoryLoginDialog dlg = new RemoteRepositoryLoginDialog(Display.getDefault().getActiveShell());
+               if (dlg.open() == Dialog.OK) {
+                       view.refresh(null);
+               }
+       }
+
+       // public void setRepositoryFactory(RepositoryFactory repositoryFactory) {
+       // this.repositoryFactory = repositoryFactory;
+       // }
+       //
+       // public void setKeyring(Keyring keyring) {
+       // this.keyring = keyring;
+       // }
+       //
+       // public void setNodeRepository(Repository nodeRepository) {
+       // this.nodeRepository = nodeRepository;
+       // }
+
+       class RemoteRepositoryLoginDialog extends TitleAreaDialog {
+               private static final long serialVersionUID = 2234006887750103399L;
+               private Text name;
+               private Text uri;
+               private Text username;
+               private Text password;
+               private Button saveInKeyring;
+
+               public RemoteRepositoryLoginDialog(Shell parentShell) {
+                       super(parentShell);
+               }
+
+               protected Point getInitialSize() {
+                       return new Point(600, 400);
+               }
+
+               protected Control createDialogArea(Composite parent) {
+                       Composite dialogarea = (Composite) super.createDialogArea(parent);
+                       dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       Composite composite = new Composite(dialogarea, SWT.NONE);
+                       composite.setLayout(new GridLayout(2, false));
+                       composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+                       setMessage("Login to remote repository", IMessageProvider.NONE);
+                       name = createLT(composite, "Name", "remoteRepository");
+                       uri = createLT(composite, "URI", "http://localhost:7070/jcr/node");
+                       username = createLT(composite, "User", "");
+                       password = createLP(composite, "Password");
+
+                       saveInKeyring = createLC(composite, "Remember password", false);
+                       parent.pack();
+                       return composite;
+               }
+
+               @Override
+               protected void createButtonsForButtonBar(Composite parent) {
+                       super.createButtonsForButtonBar(parent);
+                       Button test = createButton(parent, 2, "Test", false);
+                       test.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = -1829962269440419560L;
+
+                               public void widgetSelected(SelectionEvent arg0) {
+                                       testConnection();
+                               }
+                       });
+               }
+
+               void testConnection() {
+                       Session session = null;
+                       try {
+                               URI checkedUri = new URI(uri.getText());
+                               String checkedUriStr = checkedUri.toString();
+
+                               Hashtable<String, String> params = new Hashtable<String, String>();
+                               params.put(CmsConstants.LABELED_URI, checkedUriStr);
+                               Repository repository = repositoryFactory.getRepository(params);
+                               if (username.getText().trim().equals("")) {// anonymous
+                                       // FIXME make it more generic
+                                       session = repository.login(CmsConstants.SYS_WORKSPACE);
+                               } else {
+                                       // FIXME use getTextChars() when upgrading to 3.7
+                                       // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=297412
+                                       char[] pwd = password.getText().toCharArray();
+                                       SimpleCredentials sc = new SimpleCredentials(username.getText(), pwd);
+                                       session = repository.login(sc, "main");
+                                       MessageDialog.openInformation(getParentShell(), "Success",
+                                                       "Connection to '" + uri.getText() + "' successful");
+                               }
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Connection test failed for " + uri.getText(), e);
+                       } finally {
+                               JcrUtils.logoutQuietly(session);
+                       }
+               }
+
+               @Override
+               protected void okPressed() {
+                       Session nodeSession = null;
+                       try {
+                               nodeSession = nodeRepository.login();
+                               Node home = CmsJcrUtils.getUserHome(nodeSession);
+
+                               Node remote = home.hasNode(ArgeoNames.ARGEO_REMOTE) ? home.getNode(ArgeoNames.ARGEO_REMOTE)
+                                               : home.addNode(ArgeoNames.ARGEO_REMOTE);
+                               if (remote.hasNode(name.getText()))
+                                       throw new EclipseUiException("There is already a remote repository named " + name.getText());
+                               Node remoteRepository = remote.addNode(name.getText(), ArgeoTypes.ARGEO_REMOTE_REPOSITORY);
+                               remoteRepository.setProperty(ArgeoNames.ARGEO_URI, uri.getText());
+                               remoteRepository.setProperty(ArgeoNames.ARGEO_USER_ID, username.getText());
+                               nodeSession.save();
+                               if (saveInKeyring.getSelection()) {
+                                       String pwdPath = remoteRepository.getPath() + '/' + ArgeoNames.ARGEO_PASSWORD;
+                                       keyring.set(pwdPath, password.getText().toCharArray());
+                               }
+                               nodeSession.save();
+                               MessageDialog.openInformation(getParentShell(), "Repository Added",
+                                               "Remote repository '" + username.getText() + "@" + uri.getText() + "' added");
+
+                               super.okPressed();
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Cannot add remote repository", e);
+                       } finally {
+                               JcrUtils.logoutQuietly(nodeSession);
+                       }
+               }
+
+               /** Creates label and text. */
+               protected Text createLT(Composite parent, String label, String initial) {
+                       new Label(parent, SWT.NONE).setText(label);
+                       Text text = new Text(parent, SWT.SINGLE | SWT.LEAD | SWT.BORDER);
+                       text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       text.setText(initial);
+                       return text;
+               }
+
+               /** Creates label and check. */
+               protected Button createLC(Composite parent, String label, Boolean initial) {
+                       new Label(parent, SWT.NONE).setText(label);
+                       Button check = new Button(parent, SWT.CHECK);
+                       check.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       check.setSelection(initial);
+                       return check;
+               }
+
+               protected Text createLP(Composite parent, String label) {
+                       new Label(parent, SWT.NONE).setText(label);
+                       Text text = new Text(parent, SWT.SINGLE | SWT.LEAD | SWT.BORDER | SWT.PASSWORD);
+                       text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       return text;
+               }
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/DeleteNodes.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/DeleteNodes.java
new file mode 100644 (file)
index 0000000..b8de06b
--- /dev/null
@@ -0,0 +1,95 @@
+package org.argeo.cms.e4.jcr.handlers;
+
+import java.util.List;
+
+import javax.inject.Named;
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.e4.jcr.JcrBrowserView;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.jcr.model.WorkspaceElem;
+import org.argeo.cms.ux.widgets.TreeParent;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.services.IServiceConstants;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.widgets.Display;
+
+/**
+ * Delete the selected nodes: both in the JCR repository and in the UI view.
+ * Warning no check is done, except implementation dependent native checks,
+ * handle with care.
+ * 
+ * This handler is still 'hard linked' to a GenericJcrBrowser view to enable
+ * correct tree refresh when a node is added. This must be corrected in future
+ * versions.
+ */
+public class DeleteNodes {
+       @Execute
+       public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) {
+               List<?> selection = (List<?>) selectionService.getSelection();
+               if (selection == null)
+                       return;
+
+               JcrBrowserView view = (JcrBrowserView) part.getObject();
+
+               // confirmation
+               StringBuffer buf = new StringBuffer("");
+               for (Object o : selection) {
+                       SingleJcrNodeElem sjn = (SingleJcrNodeElem) o;
+                       buf.append(sjn.getName()).append(' ');
+               }
+               Boolean doRemove = MessageDialog.openConfirm(Display.getCurrent().getActiveShell(), "Confirm deletion",
+                               "Do you want to delete " + buf + "?");
+
+               // operation
+               if (doRemove) {
+                       SingleJcrNodeElem ancestor = null;
+                       WorkspaceElem rootAncestor = null;
+                       try {
+                               for (Object obj : selection) {
+                                       if (obj instanceof SingleJcrNodeElem) {
+                                               // Cache objects
+                                               SingleJcrNodeElem sjn = (SingleJcrNodeElem) obj;
+                                               TreeParent tp = (TreeParent) sjn.getParent();
+                                               Node node = sjn.getNode();
+
+                                               // Jcr Remove
+                                               node.remove();
+                                               node.getSession().save();
+                                               // UI remove
+                                               tp.removeChild(sjn);
+
+                                               // Check if the parent is the root node
+                                               if (tp instanceof WorkspaceElem)
+                                                       rootAncestor = (WorkspaceElem) tp;
+                                               else
+                                                       ancestor = getOlder(ancestor, (SingleJcrNodeElem) tp);
+                                       }
+                               }
+                               if (rootAncestor != null)
+                                       view.nodeRemoved(rootAncestor);
+                               else if (ancestor != null)
+                                       view.nodeRemoved(ancestor);
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Cannot delete selected node ", e);
+                       }
+               }
+       }
+
+       private SingleJcrNodeElem getOlder(SingleJcrNodeElem A, SingleJcrNodeElem B) {
+               try {
+                       if (A == null)
+                               return B == null ? null : B;
+                       // Todo enhanced this method
+                       else
+                               return A.getNode().getDepth() <= B.getNode().getDepth() ? A : B;
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Cannot find ancestor", re);
+               }
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/Refresh.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/Refresh.java
new file mode 100644 (file)
index 0000000..036e70a
--- /dev/null
@@ -0,0 +1,43 @@
+package org.argeo.cms.e4.jcr.handlers;
+
+import java.util.List;
+
+import javax.inject.Named;
+
+import org.argeo.cms.e4.jcr.JcrBrowserView;
+import org.argeo.cms.ui.jcr.JcrBrowserUtils;
+import org.argeo.cms.ux.widgets.TreeParent;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.services.IServiceConstants;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+
+/**
+ * Force the selected objects of the active view to be refreshed doing the
+ * following:
+ * <ol>
+ * <li>The model objects are recomputed</li>
+ * <li>the view is refreshed</li>
+ * </ol>
+ */
+public class Refresh {
+
+       @Execute
+       public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, EPartService partService,
+                       ESelectionService selectionService) {
+
+               JcrBrowserView view = (JcrBrowserView) part.getObject();
+               List<?> selection = (List<?>) selectionService.getSelection();
+
+               if (selection != null && !selection.isEmpty()) {
+                       for (Object obj : selection)
+                               if (obj instanceof TreeParent) {
+                                       TreeParent tp = (TreeParent) obj;
+                                       JcrBrowserUtils.forceRefreshIfNeeded(tp);
+                                       view.refresh(obj);
+                               }
+               } else if (view instanceof JcrBrowserView)
+                       view.refresh(null); // force full refresh
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/RenameNode.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/RenameNode.java
new file mode 100644 (file)
index 0000000..97674ab
--- /dev/null
@@ -0,0 +1,59 @@
+package org.argeo.cms.e4.jcr.handlers;
+
+import java.util.List;
+
+import javax.inject.Named;
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.cms.e4.jcr.JcrBrowserView;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.dialogs.SingleValue;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.services.IServiceConstants;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+
+/**
+ * Canonically call JCR Session#move(String, String) on the first element
+ * returned by HandlerUtil#getActiveWorkbenchWindow()
+ * (...getActivePage().getSelection()), if it is a {@link SingleJcrNodeElem}.
+ * The user must then fill a new name in and confirm
+ */
+public class RenameNode {
+       @Execute
+       public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, EPartService partService,
+                       ESelectionService selectionService) {
+               List<?> selection = (List<?>) selectionService.getSelection();
+               if (selection == null || selection.size() != 1)
+                       return;
+               JcrBrowserView view = (JcrBrowserView) part.getObject();
+
+               Object element = selection.get(0);
+               if (element instanceof SingleJcrNodeElem) {
+                       SingleJcrNodeElem sjn = (SingleJcrNodeElem) element;
+                       Node node = sjn.getNode();
+                       Session session = null;
+                       String newName = null;
+                       String oldPath = null;
+                       try {
+                               newName = SingleValue.ask("New node name", "Please provide a new name for [" + node.getName() + "]");
+                               // TODO sanity check and user feedback
+                               newName = JcrUtils.replaceInvalidChars(newName);
+                               oldPath = node.getPath();
+                               session = node.getSession();
+                               session.move(oldPath, JcrUtils.parentPath(oldPath) + "/" + newName);
+                               session.save();
+
+                               // Manually refresh the browser view. Must be enhanced
+                               view.refresh(sjn);
+                       } catch (RepositoryException e) {
+                               throw new EclipseUiException("Unable to rename " + node + " to " + newName, e);
+                       }
+               }
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/package-info.java
new file mode 100644 (file)
index 0000000..4e075e2
--- /dev/null
@@ -0,0 +1,2 @@
+/** JCR browser handlers. */
+package org.argeo.cms.e4.jcr.handlers;
\ No newline at end of file
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/package-info.java
new file mode 100644 (file)
index 0000000..3e92fb0
--- /dev/null
@@ -0,0 +1,2 @@
+/** JCR browser perspective. */
+package org.argeo.cms.e4.jcr;
\ No newline at end of file
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/AbstractOsgiComposite.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/AbstractOsgiComposite.java
new file mode 100644 (file)
index 0000000..4fd1d68
--- /dev/null
@@ -0,0 +1,41 @@
+package org.argeo.cms.e4.maintenance;
+
+import java.util.Collection;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+
+abstract class AbstractOsgiComposite extends Composite {
+       private static final long serialVersionUID = -4097415973477517137L;
+       protected final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
+       protected final CmsLog log = CmsLog.getLog(getClass());
+
+       public AbstractOsgiComposite(Composite parent, int style) {
+               super(parent, style);
+               parent.setLayout(CmsSwtUtils.noSpaceGridLayout());
+               setLayout(CmsSwtUtils.noSpaceGridLayout());
+               setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
+               initUi(style);
+       }
+
+       protected abstract void initUi(int style);
+
+       protected <T> T getService(Class<? extends T> clazz) {
+               return bc.getService(bc.getServiceReference(clazz));
+       }
+
+       protected <T> Collection<ServiceReference<T>> getServiceReferences(Class<T> clazz, String filter) {
+               try {
+                       return bc.getServiceReferences(clazz, filter);
+               } catch (InvalidSyntaxException e) {
+                       throw new IllegalArgumentException("Filter " + filter + " is invalid", e);
+               }
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/Browse.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/Browse.java
new file mode 100644 (file)
index 0000000..a536da0
--- /dev/null
@@ -0,0 +1,576 @@
+package org.argeo.cms.e4.maintenance;
+
+import static org.eclipse.swt.SWT.RIGHT;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.LinkedHashMap;
+
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+
+import org.argeo.api.cms.ux.Cms2DSize;
+import org.argeo.cms.swt.CmsException;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.ui.CmsUiProvider;
+import org.argeo.cms.ui.util.CmsLink;
+import org.argeo.cms.ui.widgets.EditableImage;
+import org.argeo.cms.ui.widgets.Img;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ILazyContentProvider;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.Text;
+
+public class Browse implements CmsUiProvider {
+
+       // Some local constants to experiment. should be cleaned
+       private final static String BROWSE_PREFIX = "browse#";
+       private final static int THUMBNAIL_WIDTH = 400;
+       private final static int COLUMN_WIDTH = 160;
+       private DateFormat timeFormatter = new SimpleDateFormat("dd-MM-yyyy', 'HH:mm");
+
+       // keep a cache of the opened nodes
+       // Key is the path
+       private LinkedHashMap<String, FilterEntitiesVirtualTable> browserCols = new LinkedHashMap<String, Browse.FilterEntitiesVirtualTable>();
+       private Composite nodeDisplayParent;
+       private Composite colViewer;
+       private ScrolledComposite scrolledCmp;
+       private Text parentPathTxt;
+       private Text filterTxt;
+       private Node currEdited;
+
+       private String initialPath;
+
+       @Override
+       public Control createUi(Composite parent, Node context) throws RepositoryException {
+               if (context == null)
+                       // return null;
+                       throw new CmsException("Context cannot be null");
+               GridLayout layout = CmsSwtUtils.noSpaceGridLayout();
+               layout.numColumns = 2;
+               parent.setLayout(layout);
+
+               // Left
+               Composite leftCmp = new Composite(parent, SWT.NO_FOCUS);
+               leftCmp.setLayoutData(CmsSwtUtils.fillAll());
+               createBrowserPart(leftCmp, context);
+
+               // Right
+               nodeDisplayParent = new Composite(parent, SWT.NO_FOCUS | SWT.BORDER);
+               GridData gd = new GridData(SWT.RIGHT, SWT.FILL, false, true);
+               gd.widthHint = THUMBNAIL_WIDTH;
+               nodeDisplayParent.setLayoutData(gd);
+               createNodeView(nodeDisplayParent, context);
+
+               // INIT
+               setEdited(context);
+               initialPath = context.getPath();
+
+               // Workaround we don't yet manage the delete to display parent of the
+               // initial context node
+
+               return null;
+       }
+
+       private void createBrowserPart(Composite parent, Node context) throws RepositoryException {
+               GridLayout layout = CmsSwtUtils.noSpaceGridLayout();
+               parent.setLayout(layout);
+               Composite filterCmp = new Composite(parent, SWT.NO_FOCUS);
+               filterCmp.setLayoutData(CmsSwtUtils.fillWidth());
+
+               // top filter
+               addFilterPanel(filterCmp);
+
+               // scrolled composite
+               scrolledCmp = new ScrolledComposite(parent, SWT.H_SCROLL | SWT.BORDER | SWT.NO_FOCUS);
+               scrolledCmp.setLayoutData(CmsSwtUtils.fillAll());
+               scrolledCmp.setExpandVertical(true);
+               scrolledCmp.setExpandHorizontal(true);
+               scrolledCmp.setShowFocusedControl(true);
+
+               colViewer = new Composite(scrolledCmp, SWT.NO_FOCUS);
+               scrolledCmp.setContent(colViewer);
+               scrolledCmp.addControlListener(new ControlAdapter() {
+                       private static final long serialVersionUID = 6589392045145698201L;
+
+                       @Override
+                       public void controlResized(ControlEvent e) {
+                               Rectangle r = scrolledCmp.getClientArea();
+                               scrolledCmp.setMinSize(colViewer.computeSize(SWT.DEFAULT, r.height));
+                       }
+               });
+               initExplorer(colViewer, context);
+       }
+
+       private Control initExplorer(Composite parent, Node context) throws RepositoryException {
+               parent.setLayout(CmsSwtUtils.noSpaceGridLayout());
+               createBrowserColumn(parent, context);
+               return null;
+       }
+
+       private Control createBrowserColumn(Composite parent, Node context) throws RepositoryException {
+               // TODO style is not correctly managed.
+               FilterEntitiesVirtualTable table = new FilterEntitiesVirtualTable(parent, SWT.BORDER | SWT.NO_FOCUS, context);
+               // CmsUiUtils.style(table, ArgeoOrgStyle.browserColumn.style());
+               table.filterList("*");
+               table.setLayoutData(new GridData(SWT.LEFT, SWT.FILL, false, true));
+               browserCols.put(context.getPath(), table);
+               return null;
+       }
+
+       public void addFilterPanel(Composite parent) {
+
+               parent.setLayout(CmsSwtUtils.noSpaceGridLayout(new GridLayout(2, false)));
+
+               // Text Area for the filter
+               parentPathTxt = new Text(parent, SWT.NO_FOCUS);
+               parentPathTxt.setEditable(false);
+               filterTxt = new Text(parent, SWT.SEARCH | SWT.ICON_CANCEL);
+               filterTxt.setMessage("Filter current list");
+               filterTxt.setLayoutData(CmsSwtUtils.fillWidth());
+               filterTxt.addModifyListener(new ModifyListener() {
+                       private static final long serialVersionUID = 7709303319740056286L;
+
+                       public void modifyText(ModifyEvent event) {
+                               modifyFilter(false);
+                       }
+               });
+
+               filterTxt.addKeyListener(new KeyListener() {
+                       private static final long serialVersionUID = -4523394262771183968L;
+
+                       @Override
+                       public void keyReleased(KeyEvent e) {
+                       }
+
+                       @Override
+                       public void keyPressed(KeyEvent e) {
+                               boolean shiftPressed = (e.stateMask & SWT.SHIFT) != 0;
+                               // boolean altPressed = (e.stateMask & SWT.ALT) != 0;
+                               FilterEntitiesVirtualTable currTable = null;
+                               if (currEdited != null) {
+                                       FilterEntitiesVirtualTable table = browserCols.get(getPath(currEdited));
+                                       if (table != null && !table.isDisposed())
+                                               currTable = table;
+                               }
+
+                               try {
+                                       if (e.keyCode == SWT.ARROW_DOWN)
+                                               currTable.setFocus();
+                                       else if (e.keyCode == SWT.BS) {
+                                               if (filterTxt.getText().equals("")
+                                                               && !(getPath(currEdited).equals("/") || getPath(currEdited).equals(initialPath))) {
+                                                       setEdited(currEdited.getParent());
+                                                       e.doit = false;
+                                                       filterTxt.setFocus();
+                                               }
+                                       } else if (e.keyCode == SWT.TAB && !shiftPressed) {
+                                               if (currEdited.getNodes(filterTxt.getText() + "*").getSize() == 1) {
+                                                       setEdited(currEdited.getNodes(filterTxt.getText() + "*").nextNode());
+                                               }
+                                               filterTxt.setFocus();
+                                               e.doit = false;
+                                       }
+                               } catch (RepositoryException e1) {
+                                       throw new CmsException("Unexpected error in key management for " + currEdited + "with filter "
+                                                       + filterTxt.getText(), e1);
+                               }
+
+                       }
+               });
+       }
+
+       private void setEdited(Node node) {
+               try {
+                       currEdited = node;
+                       CmsSwtUtils.clear(nodeDisplayParent);
+                       createNodeView(nodeDisplayParent, currEdited);
+                       nodeDisplayParent.layout();
+                       refreshFilters(node);
+                       refreshBrowser(node);
+               } catch (RepositoryException re) {
+                       throw new CmsException("Unable to update browser for " + node, re);
+               }
+       }
+
+       private void refreshFilters(Node node) throws RepositoryException {
+               String currNodePath = node.getPath();
+               parentPathTxt.setText(currNodePath);
+               filterTxt.setText("");
+               filterTxt.getParent().layout();
+       }
+
+       private void refreshBrowser(Node node) throws RepositoryException {
+
+               // Retrieve
+               String currNodePath = node.getPath();
+               String currParPath = "";
+               if (!"/".equals(currNodePath))
+                       currParPath = JcrUtils.parentPath(currNodePath);
+               if ("".equals(currParPath))
+                       currParPath = "/";
+
+               Object[][] colMatrix = new Object[browserCols.size()][2];
+
+               int i = 0, j = -1, k = -1;
+               for (String path : browserCols.keySet()) {
+                       colMatrix[i][0] = path;
+                       colMatrix[i][1] = browserCols.get(path);
+                       if (j >= 0 && k < 0 && !currNodePath.equals("/")) {
+                               boolean leaveOpened = path.startsWith(currNodePath);
+
+                               // workaround for same name siblings
+                               // fix me weird side effect when we go left or click on anb
+                               // already selected, unfocused node
+                               if (leaveOpened && (path.lastIndexOf("/") == 0 && currNodePath.lastIndexOf("/") == 0
+                                               || JcrUtils.parentPath(path).equals(JcrUtils.parentPath(currNodePath))))
+                                       leaveOpened = JcrUtils.lastPathElement(path).equals(JcrUtils.lastPathElement(currNodePath));
+
+                               if (!leaveOpened)
+                                       k = i;
+                       }
+                       if (currParPath.equals(path))
+                               j = i;
+                       i++;
+               }
+
+               if (j >= 0 && k >= 0)
+                       // remove useless cols
+                       for (int l = i - 1; l >= k; l--) {
+                               browserCols.remove(colMatrix[l][0]);
+                               ((FilterEntitiesVirtualTable) colMatrix[l][1]).dispose();
+                       }
+
+               // Remove disposed columns
+               // TODO investigate and fix the mechanism that leave them there after
+               // disposal
+               if (browserCols.containsKey(currNodePath)) {
+                       FilterEntitiesVirtualTable currCol = browserCols.get(currNodePath);
+                       if (currCol.isDisposed())
+                               browserCols.remove(currNodePath);
+               }
+
+               if (!browserCols.containsKey(currNodePath))
+                       createBrowserColumn(colViewer, node);
+
+               colViewer.setLayout(CmsSwtUtils.noSpaceGridLayout(new GridLayout(browserCols.size(), false)));
+               // colViewer.pack();
+               colViewer.layout();
+               // also resize the scrolled composite
+               scrolledCmp.layout();
+               scrolledCmp.getShowFocusedControl();
+               // colViewer.getParent().layout();
+               // if (JcrUtils.parentPath(currNodePath).equals(currBrowserKey)) {
+               // } else {
+               // }
+       }
+
+       private void modifyFilter(boolean fromOutside) {
+               if (!fromOutside)
+                       if (currEdited != null) {
+                               String filter = filterTxt.getText() + "*";
+                               FilterEntitiesVirtualTable table = browserCols.get(getPath(currEdited));
+                               if (table != null && !table.isDisposed())
+                                       table.filterList(filter);
+                       }
+
+       }
+
+       private String getPath(Node node) {
+               try {
+                       return node.getPath();
+               } catch (RepositoryException e) {
+                       throw new CmsException("Unable to get path for node " + node, e);
+               }
+       }
+
+       private Cms2DSize imageWidth = new Cms2DSize(250, 0);
+
+       /**
+        * Recreates the content of the box that displays information about the current
+        * selected node.
+        */
+       private Control createNodeView(Composite parent, Node context) throws RepositoryException {
+
+               parent.setLayout(new GridLayout(2, false));
+
+               if (isImg(context)) {
+                       EditableImage image = new Img(parent, RIGHT, context, imageWidth);
+                       image.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false, 2, 1));
+               }
+
+               // Name and primary type
+               Label contextL = new Label(parent, SWT.NONE);
+               CmsSwtUtils.markup(contextL);
+               contextL.setText("<b>" + context.getName() + "</b>");
+               new Label(parent, SWT.NONE).setText(context.getPrimaryNodeType().getName());
+
+               // Children
+               for (NodeIterator nIt = context.getNodes(); nIt.hasNext();) {
+                       Node child = nIt.nextNode();
+                       new CmsLink(child.getName(), BROWSE_PREFIX + child.getPath()).createUi(parent, context);
+                       new Label(parent, SWT.NONE).setText(child.getPrimaryNodeType().getName());
+               }
+
+               // Properties
+               for (PropertyIterator pIt = context.getProperties(); pIt.hasNext();) {
+                       Property property = pIt.nextProperty();
+                       Label label = new Label(parent, SWT.NONE);
+                       label.setText(property.getName());
+                       label.setToolTipText(JcrUtils.getPropertyDefinitionAsString(property));
+                       new Label(parent, SWT.NONE).setText(getPropAsString(property));
+               }
+
+               return null;
+       }
+
+       private boolean isImg(Node node) throws RepositoryException {
+               // TODO support images
+               return false;
+//             return node.hasNode(JCR_CONTENT) && node.isNodeType(CmsTypes.CMS_IMAGE);
+       }
+
+       private String getPropAsString(Property property) throws RepositoryException {
+               String result = "";
+               if (property.isMultiple()) {
+                       result = getMultiAsString(property, ", ");
+               } else {
+                       Value value = property.getValue();
+                       if (value.getType() == PropertyType.BINARY)
+                               result = "<binary>";
+                       else if (value.getType() == PropertyType.DATE)
+                               result = timeFormatter.format(value.getDate().getTime());
+                       else
+                               result = value.getString();
+               }
+               return result;
+       }
+
+       private String getMultiAsString(Property property, String separator) throws RepositoryException {
+               if (separator == null)
+                       separator = "; ";
+               Value[] values = property.getValues();
+               StringBuilder builder = new StringBuilder();
+               for (Value val : values) {
+                       String currStr = val.getString();
+                       if (!"".equals(currStr.trim()))
+                               builder.append(currStr).append(separator);
+               }
+               if (builder.lastIndexOf(separator) >= 0)
+                       return builder.substring(0, builder.length() - separator.length());
+               else
+                       return builder.toString();
+       }
+
+       /** Almost canonical implementation of a table that display entities */
+       private class FilterEntitiesVirtualTable extends Composite {
+               private static final long serialVersionUID = 8798147431706283824L;
+
+               // Context
+               private Node context;
+
+               // UI Objects
+               private TableViewer entityViewer;
+
+               // enable management of multiple columns
+               Node getNode() {
+                       return context;
+               }
+
+               @Override
+               public boolean setFocus() {
+                       if (entityViewer.getTable().isDisposed())
+                               return false;
+                       if (entityViewer.getSelection().isEmpty()) {
+                               Object first = entityViewer.getElementAt(0);
+                               if (first != null) {
+                                       entityViewer.setSelection(new StructuredSelection(first), true);
+                               }
+                       }
+                       return entityViewer.getTable().setFocus();
+               }
+
+               void filterList(String filter) {
+                       try {
+                               NodeIterator nit = context.getNodes(filter);
+                               refreshFilteredList(nit);
+                       } catch (RepositoryException e) {
+                               throw new CmsException("Unable to filter " + getNode() + " children with filter " + filter, e);
+                       }
+
+               }
+
+               public FilterEntitiesVirtualTable(Composite parent, int style, Node context) {
+                       super(parent, SWT.NO_FOCUS);
+                       this.context = context;
+                       populate();
+               }
+
+               protected void populate() {
+                       Composite parent = this;
+                       GridLayout layout = CmsSwtUtils.noSpaceGridLayout();
+
+                       this.setLayout(layout);
+                       createTableViewer(parent);
+               }
+
+               private void createTableViewer(final Composite parent) {
+                       // the list
+                       // We must limit the size of the table otherwise the full list is
+                       // loaded
+                       // before the layout happens
+                       Composite listCmp = new Composite(parent, SWT.NO_FOCUS);
+                       GridData gd = new GridData(SWT.LEFT, SWT.FILL, false, true);
+                       gd.widthHint = COLUMN_WIDTH;
+                       listCmp.setLayoutData(gd);
+                       listCmp.setLayout(CmsSwtUtils.noSpaceGridLayout());
+
+                       entityViewer = new TableViewer(listCmp, SWT.VIRTUAL | SWT.SINGLE);
+                       Table table = entityViewer.getTable();
+
+                       table.setLayoutData(CmsSwtUtils.fillAll());
+                       table.setLinesVisible(true);
+                       table.setHeaderVisible(false);
+                       CmsSwtUtils.markup(table);
+
+                       CmsSwtUtils.style(table, MaintenanceStyles.BROWSER_COLUMN);
+
+                       // first column
+                       TableViewerColumn column = new TableViewerColumn(entityViewer, SWT.NONE);
+                       TableColumn tcol = column.getColumn();
+                       tcol.setWidth(COLUMN_WIDTH);
+                       tcol.setResizable(true);
+                       column.setLabelProvider(new SimpleNameLP());
+
+                       entityViewer.setContentProvider(new MyLazyCP(entityViewer));
+                       entityViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+
+                               @Override
+                               public void selectionChanged(SelectionChangedEvent event) {
+                                       IStructuredSelection selection = (IStructuredSelection) entityViewer.getSelection();
+                                       if (selection.isEmpty())
+                                               return;
+                                       else
+                                               setEdited((Node) selection.getFirstElement());
+
+                               }
+                       });
+
+                       table.addKeyListener(new KeyListener() {
+                               private static final long serialVersionUID = -330694313896036230L;
+
+                               @Override
+                               public void keyReleased(KeyEvent e) {
+                               }
+
+                               @Override
+                               public void keyPressed(KeyEvent e) {
+
+                                       IStructuredSelection selection = (IStructuredSelection) entityViewer.getSelection();
+                                       Node selected = null;
+                                       if (!selection.isEmpty())
+                                               selected = ((Node) selection.getFirstElement());
+                                       try {
+                                               if (e.keyCode == SWT.ARROW_RIGHT) {
+                                                       if (selected != null) {
+                                                               setEdited(selected);
+                                                               browserCols.get(selected.getPath()).setFocus();
+                                                       }
+                                               } else if (e.keyCode == SWT.ARROW_LEFT) {
+                                                       try {
+                                                               selected = getNode().getParent();
+                                                               String newPath = selected.getPath(); // getNode().getParent()
+                                                               setEdited(selected);
+                                                               if (browserCols.containsKey(newPath))
+                                                                       browserCols.get(newPath).setFocus();
+                                                       } catch (ItemNotFoundException ie) {
+                                                               // root silent
+                                                       }
+                                               }
+                                       } catch (RepositoryException ie) {
+                                               throw new CmsException("Error while managing arrow " + "events in the browser for " + selected,
+                                                               ie);
+                                       }
+                               }
+                       });
+               }
+
+               private class MyLazyCP implements ILazyContentProvider {
+                       private static final long serialVersionUID = 1L;
+                       private TableViewer viewer;
+                       private Object[] elements;
+
+                       public MyLazyCP(TableViewer viewer) {
+                               this.viewer = viewer;
+                       }
+
+                       public void dispose() {
+                       }
+
+                       public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+                               // IMPORTANT: don't forget this: an exception will be thrown if
+                               // a selected object is not part of the results anymore.
+                               viewer.setSelection(null);
+                               this.elements = (Object[]) newInput;
+                       }
+
+                       public void updateElement(int index) {
+                               viewer.replace(elements[index], index);
+                       }
+               }
+
+               protected void refreshFilteredList(NodeIterator children) {
+                       Object[] rows = JcrUtils.nodeIteratorToList(children).toArray();
+                       entityViewer.setInput(rows);
+                       entityViewer.setItemCount(rows.length);
+                       entityViewer.refresh();
+               }
+
+               public class SimpleNameLP extends ColumnLabelProvider {
+                       private static final long serialVersionUID = 2465059387875338553L;
+
+                       @Override
+                       public String getText(Object element) {
+                               if (element instanceof Node) {
+                                       Node curr = ((Node) element);
+                                       try {
+                                               return curr.getName();
+                                       } catch (RepositoryException e) {
+                                               throw new CmsException("Unable to get name for" + curr);
+                                       }
+                               }
+                               return super.getText(element);
+                       }
+               }
+       }
+}
\ No newline at end of file
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/ConnectivityDeploymentUi.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/ConnectivityDeploymentUi.java
new file mode 100644 (file)
index 0000000..97f3e67
--- /dev/null
@@ -0,0 +1,48 @@
+package org.argeo.cms.e4.maintenance;
+
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.http.HttpService;
+import org.osgi.service.useradmin.UserAdmin;
+
+class ConnectivityDeploymentUi extends AbstractOsgiComposite {
+       private static final long serialVersionUID = 590221539553514693L;
+
+       public ConnectivityDeploymentUi(Composite parent, int style) {
+               super(parent, style);
+       }
+
+       @Override
+       protected void initUi(int style) {
+               StringBuffer text = new StringBuffer();
+               text.append("<span style='font-variant: small-caps;'>Provided Servers</span><br/>");
+
+               ServiceReference<HttpService> userAdminRef = bc.getServiceReference(HttpService.class);
+               if (userAdminRef != null) {
+                       // FIXME use constants
+                       Object httpPort = userAdminRef.getProperty("http.port");
+                       Object httpsPort = userAdminRef.getProperty("https.port");
+                       if (httpPort != null)
+                               text.append("<b>http</b> ").append(httpPort).append("<br/>");
+                       if (httpsPort != null)
+                               text.append("<b>https</b> ").append(httpsPort).append("<br/>");
+
+               }
+
+               text.append("<br/>");
+               text.append("<span style='font-variant: small-caps;'>Referenced Servers</span><br/>");
+
+               Label label = new Label(this, SWT.NONE);
+               label.setData(new GridData(SWT.FILL, SWT.FILL, false, false));
+               CmsSwtUtils.markup(label);
+               label.setText(text.toString());
+       }
+
+       protected boolean isDeployed() {
+               return bc.getServiceReference(UserAdmin.class) != null;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/DataDeploymentUi.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/DataDeploymentUi.java
new file mode 100644 (file)
index 0000000..ef95bde
--- /dev/null
@@ -0,0 +1,139 @@
+package org.argeo.cms.e4.maintenance;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileStore;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+
+import org.apache.jackrabbit.core.RepositoryContext;
+import org.apache.jackrabbit.core.config.RepositoryConfig;
+import org.argeo.api.cms.CmsConstants;
+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.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.osgi.framework.ServiceReference;
+
+class DataDeploymentUi extends AbstractOsgiComposite {
+       private static final long serialVersionUID = 590221539553514693L;
+
+       public DataDeploymentUi(Composite parent, int style) {
+               super(parent, style);
+       }
+
+       @Override
+       protected void initUi(int style) {
+               if (isDeployed()) {
+                       initCurrentUi(this);
+               } else {
+                       initNewUi(this);
+               }
+       }
+
+       private void initNewUi(Composite parent) {
+//             try {
+//                     ConfigurationAdmin confAdmin = bc.getService(bc.getServiceReference(ConfigurationAdmin.class));
+//                     Configuration[] confs = confAdmin.listConfigurations(
+//                                     "(" + ConfigurationAdmin.SERVICE_FACTORYPID + "=" + NodeConstants.NODE_REPOS_FACTORY_PID + ")");
+//                     if (confs == null || confs.length == 0) {
+//                             Group buttonGroup = new Group(parent, SWT.NONE);
+//                             buttonGroup.setText("Repository Type");
+//                             buttonGroup.setLayout(new GridLayout(2, true));
+//                             buttonGroup.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+//
+//                             SelectionListener selectionListener = new SelectionAdapter() {
+//                                     private static final long serialVersionUID = 6247064348421088092L;
+//
+//                                     public void widgetSelected(SelectionEvent event) {
+//                                             Button radio = (Button) event.widget;
+//                                             if (!radio.getSelection())
+//                                                     return;
+//                                             log.debug(event);
+//                                             JackrabbitType nodeType = (JackrabbitType) radio.getData();
+//                                             if (log.isDebugEnabled())
+//                                                     log.debug(" selected = " + nodeType.name());
+//                                     };
+//                             };
+//
+//                             for (JackrabbitType nodeType : JackrabbitType.values()) {
+//                                     Button radio = new Button(buttonGroup, SWT.RADIO);
+//                                     radio.setText(nodeType.name());
+//                                     radio.setData(nodeType);
+//                                     if (nodeType.equals(JackrabbitType.localfs))
+//                                             radio.setSelection(true);
+//                                     radio.addSelectionListener(selectionListener);
+//                             }
+//
+//                     } else if (confs.length == 1) {
+//
+//                     } else {
+//                             throw new CmsException("Multiple repos not yet supported");
+//                     }
+//             } catch (Exception e) {
+//                     throw new CmsException("Cannot initialize UI", e);
+//             }
+
+       }
+
+       private void initCurrentUi(Composite parent) {
+               parent.setLayout(new GridLayout());
+               Collection<ServiceReference<RepositoryContext>> contexts = getServiceReferences(RepositoryContext.class,
+                               "(" + CmsConstants.CN + "=*)");
+               StringBuffer text = new StringBuffer();
+               text.append("<span style='font-variant: small-caps;'>Jackrabbit Repositories</span><br/>");
+               for (ServiceReference<RepositoryContext> sr : contexts) {
+                       RepositoryContext repositoryContext = bc.getService(sr);
+                       String alias = sr.getProperty(CmsConstants.CN).toString();
+                       String rootNodeId = repositoryContext.getRootNodeId().toString();
+                       RepositoryConfig repositoryConfig = repositoryContext.getRepositoryConfig();
+                       Path repoHomePath = new File(repositoryConfig.getHomeDir()).toPath().toAbsolutePath();
+                       // TODO check data store
+
+                       text.append("<b>" + alias + "</b><br/>");
+                       text.append("rootNodeId: " + rootNodeId + "<br/>");
+                       try {
+                               FileStore fileStore = Files.getFileStore(repoHomePath);
+                               text.append("partition: " + fileStore.toString() + "<br/>");
+                               text.append(
+                                               percentUsed(fileStore) + " used (" + humanReadable(fileStore.getUsableSpace()) + " free)<br/>");
+                       } catch (IOException e) {
+                               log.error("Cannot check fileStore for " + repoHomePath, e);
+                       }
+               }
+               Label label = new Label(parent, SWT.NONE);
+               label.setData(new GridData(SWT.FILL, SWT.FILL, false, false));
+               CmsSwtUtils.markup(label);
+               label.setText("<span style=''>" + text.toString() + "</span>");
+       }
+
+       private String humanReadable(long bytes) {
+               long mb = bytes / (1024 * 1024);
+               return mb >= 2048 ? Long.toString(mb / 1024) + " GB" : Long.toString(mb) + " MB";
+       }
+
+       private String percentUsed(FileStore fs) throws IOException {
+               long used = fs.getTotalSpace() - fs.getUnallocatedSpace();
+               long percent = used * 100 / fs.getTotalSpace();
+               if (log.isTraceEnabled()) {
+                       // output identical to `df -B 1`)
+                       log.trace(fs.getTotalSpace() + "," + used + "," + fs.getUsableSpace());
+               }
+               String span;
+               if (percent < 80)
+                       span = "<span style='color:green;font-weight:bold'>";
+               else if (percent < 95)
+                       span = "<span style='color:orange;font-weight:bold'>";
+               else
+                       span = "<span style='color:red;font-weight:bold'>";
+               return span + percent + "%</span>";
+       }
+
+       protected boolean isDeployed() {
+               return bc.getServiceReference(RepositoryContext.class) != null;
+       }
+
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/DeploymentEntryPoint.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/DeploymentEntryPoint.java
new file mode 100644 (file)
index 0000000..0a28dc5
--- /dev/null
@@ -0,0 +1,95 @@
+package org.argeo.cms.e4.maintenance;
+
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.CmsContext;
+import org.argeo.api.cms.CmsDeployment;
+import org.argeo.api.cms.CmsState;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+
+class DeploymentEntryPoint {
+       private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
+
+       protected void createContents(Composite parent) {
+               // FIXME manage authentication if needed
+               // if (!CurrentUser.roles().contains(AuthConstants.ROLE_ADMIN))
+               // return;
+
+               // parent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               if (isDesktop()) {
+                       parent.setLayout(new GridLayout(2, true));
+               } else {
+                       // TODO add scrolling
+                       parent.setLayout(new GridLayout(1, true));
+               }
+
+               initHighLevelSummary(parent);
+
+               Group securityGroup = createHighLevelGroup(parent, "Security");
+               securityGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
+               new SecurityDeploymentUi(securityGroup, SWT.NONE);
+
+               Group dataGroup = createHighLevelGroup(parent, "Data");
+               dataGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
+               new DataDeploymentUi(dataGroup, SWT.NONE);
+
+               Group logGroup = createHighLevelGroup(parent, "Notifications");
+               logGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, true));
+               new LogDeploymentUi(logGroup, SWT.NONE);
+
+               Group connectivityGroup = createHighLevelGroup(parent, "Connectivity");
+               new ConnectivityDeploymentUi(connectivityGroup, SWT.NONE);
+               connectivityGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, true));
+
+       }
+
+       private void initHighLevelSummary(Composite parent) {
+               Composite composite = new Composite(parent, SWT.NONE);
+               GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false);
+               if (isDesktop())
+                       gridData.horizontalSpan = 3;
+               composite.setLayoutData(gridData);
+               composite.setLayout(new FillLayout());
+
+               ServiceReference<CmsState> nodeStateRef = bc.getServiceReference(CmsState.class);
+               if (nodeStateRef == null)
+                       throw new IllegalStateException("No CMS state available");
+               CmsState nodeState = bc.getService(nodeStateRef);
+               ServiceReference<CmsContext> nodeDeploymentRef = bc.getServiceReference(CmsContext.class);
+               Label label = new Label(composite, SWT.WRAP);
+               CmsSwtUtils.markup(label);
+               if (nodeDeploymentRef == null) {
+                       label.setText("Not yet deployed on, please configure below.");
+               } else {
+                       Object stateUuid = nodeStateRef.getProperty(CmsConstants.CN);
+                       CmsContext nodeDeployment = bc.getService(nodeDeploymentRef);
+                       GregorianCalendar calendar = new GregorianCalendar();
+                       calendar.setTimeInMillis(nodeDeployment.getAvailableSince());
+                       calendar.setTimeZone(TimeZone.getDefault());
+                       label.setText("Deployment state " + stateUuid + ", available since <b>" + calendar.getTime() + "</b>");
+               }
+       }
+
+       private static Group createHighLevelGroup(Composite parent, String text) {
+               Group group = new Group(parent, SWT.NONE);
+               group.setText(text);
+               CmsSwtUtils.markup(group);
+               return group;
+       }
+
+       private boolean isDesktop() {
+               return true;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/LogDeploymentUi.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/LogDeploymentUi.java
new file mode 100644 (file)
index 0000000..fa5d3da
--- /dev/null
@@ -0,0 +1,73 @@
+package org.argeo.cms.e4.maintenance;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Enumeration;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Text;
+import org.osgi.service.log.LogEntry;
+import org.osgi.service.log.LogListener;
+import org.osgi.service.log.LogReaderService;
+
+class LogDeploymentUi extends AbstractOsgiComposite implements LogListener {
+       private static final long serialVersionUID = 590221539553514693L;
+
+       private DateFormat dateFormat = new SimpleDateFormat("MMdd HH:mm");
+
+       private Display display;
+       private Text logDisplay;
+
+       public LogDeploymentUi(Composite parent, int style) {
+               super(parent, style);
+       }
+
+       @Override
+       protected void initUi(int style) {
+               LogReaderService logReader = getService(LogReaderService.class);
+               // FIXME use server push
+               // logReader.addLogListener(this);
+               this.display = getDisplay();
+               this.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               logDisplay = new Text(this, SWT.WRAP | SWT.MULTI | SWT.READ_ONLY);
+               logDisplay.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               CmsSwtUtils.markup(logDisplay);
+               Enumeration<LogEntry> logEntries = (Enumeration<LogEntry>) logReader.getLog();
+               while (logEntries.hasMoreElements())
+                       logDisplay.append(printEntry(logEntries.nextElement()));
+       }
+
+       private String printEntry(LogEntry entry) {
+               StringBuilder sb = new StringBuilder();
+               GregorianCalendar calendar = new GregorianCalendar(TimeZone.getDefault());
+               calendar.setTimeInMillis(entry.getTime());
+               sb.append(dateFormat.format(calendar.getTime())).append(' ');
+               sb.append(entry.getMessage());
+               sb.append('\n');
+               return sb.toString();
+       }
+
+       @Override
+       public void logged(LogEntry entry) {
+               if (display.isDisposed())
+                       return;
+               display.asyncExec(() -> {
+                       if (logDisplay.isDisposed())
+                               return;
+                       logDisplay.append(printEntry(entry));
+               });
+               display.wake();
+       }
+
+       // @Override
+       // public void dispose() {
+       // super.dispose();
+       // getService(LogReaderService.class).removeLogListener(this);
+       // }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/MaintenanceStyles.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/MaintenanceStyles.java
new file mode 100644 (file)
index 0000000..df1be51
--- /dev/null
@@ -0,0 +1,10 @@
+package org.argeo.cms.e4.maintenance;
+
+/** Specific styles used by the various maintenance pages . */
+public interface MaintenanceStyles {
+       // General
+       public final static String PREFIX = "maintenance_";
+
+       // Browser
+       public final static String BROWSER_COLUMN = "browser_column";
+       }
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/NonAdminPage.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/NonAdminPage.java
new file mode 100644 (file)
index 0000000..cb38ce8
--- /dev/null
@@ -0,0 +1,30 @@
+package org.argeo.cms.e4.maintenance;
+
+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.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+
+public class NonAdminPage implements CmsUiProvider{
+
+       @Override
+       public Control createUi(Composite parent, Node context)
+                       throws RepositoryException {
+               Composite body = new Composite(parent, SWT.NO_FOCUS);
+               body.setLayoutData(CmsSwtUtils.fillAll());
+               body.setLayout(new GridLayout());
+               Label label = new Label(body, SWT.NONE);
+               label.setText("You should be an admin to perform maintenance operations. "
+                               + "Are you sure you are logged in?");
+               label.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true));
+               return null;
+       }
+       
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/SecurityDeploymentUi.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/SecurityDeploymentUi.java
new file mode 100644 (file)
index 0000000..3492c54
--- /dev/null
@@ -0,0 +1,85 @@
+package org.argeo.cms.e4.maintenance;
+
+import java.net.URI;
+
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdmin;
+
+class SecurityDeploymentUi extends AbstractOsgiComposite {
+       private static final long serialVersionUID = 590221539553514693L;
+
+       public SecurityDeploymentUi(Composite parent, int style) {
+               super(parent, style);
+       }
+
+       @Override
+       protected void initUi(int style) {
+               if (isDeployed()) {
+                       initCurrentUi(this);
+               } else {
+                       initNewUi(this);
+               }
+       }
+
+       private void initNewUi(Composite parent) {
+               new Label(parent, SWT.NONE).setText("Security is not configured");
+       }
+
+       private void initCurrentUi(Composite parent) {
+               ServiceReference<UserAdmin> userAdminRef = bc.getServiceReference(UserAdmin.class);
+               UserAdmin userAdmin = bc.getService(userAdminRef);
+               StringBuffer text = new StringBuffer();
+               text.append("<span style='font-variant: small-caps;'>Domains</span><br/>");
+               domains: for (String key : userAdminRef.getPropertyKeys()) {
+                       if (!key.startsWith("/"))
+                               continue domains;
+                       URI uri;
+                       try {
+                               uri = new URI(key);
+                       } catch (Exception e) {
+                               // ignore non URI keys
+                               continue domains;
+                       }
+
+                       String rootDn = uri.getPath().substring(1, uri.getPath().length());
+                       // FIXME make reading query options more robust, using utils
+                       boolean readOnly = uri.getQuery().equals("readOnly=true");
+                       if (readOnly)
+                               text.append("<span style='font-weight:bold;font-style: italic'>");
+                       else
+                               text.append("<span style='font-weight:bold'>");
+
+                       text.append(rootDn);
+                       text.append("</span><br/>");
+                       try {
+                               Role[] roles = userAdmin.getRoles("(dn=*," + rootDn + ")");
+                               long userCount = 0;
+                               long groupCount = 0;
+                               for (Role role : roles) {
+                                       if (role.getType() == Role.USER)
+                                               userCount++;
+                                       else
+                                               groupCount++;
+                               }
+                               text.append(" " + userCount + " users, " + groupCount +" groups.<br/>");
+                       } catch (InvalidSyntaxException e) {
+                               log.error("Invalid syntax", e);
+                       }
+               }
+               Label label = new Label(parent, SWT.NONE);
+               label.setData(new GridData(SWT.FILL, SWT.FILL, false, false));
+               CmsSwtUtils.markup(label);
+               label.setText(text.toString());
+       }
+
+       protected boolean isDeployed() {
+               return bc.getServiceReference(UserAdmin.class) != null;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/package-info.java
new file mode 100644 (file)
index 0000000..e4d2ad4
--- /dev/null
@@ -0,0 +1,2 @@
+/** Maintenance perspective. */
+package org.argeo.cms.e4.maintenance;
\ No newline at end of file
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/BundleNode.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/BundleNode.java
new file mode 100644 (file)
index 0000000..e953683
--- /dev/null
@@ -0,0 +1,46 @@
+package org.argeo.cms.e4.monitoring;
+
+import org.argeo.cms.ux.widgets.TreeParent;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.ServiceReference;
+
+/** A tree element representing a {@link Bundle} */
+class BundleNode extends TreeParent {
+       private final Bundle bundle;
+
+       public BundleNode(Bundle bundle) {
+               this(bundle, false);
+       }
+
+       @SuppressWarnings("rawtypes")
+       public BundleNode(Bundle bundle, boolean hasChildren) {
+               super(bundle.getSymbolicName());
+               this.bundle = bundle;
+
+               if (hasChildren) {
+                       // REFERENCES
+                       ServiceReference[] usedServices = bundle.getServicesInUse();
+                       if (usedServices != null) {
+                               for (ServiceReference sr : usedServices) {
+                                       if (sr != null)
+                                               addChild(new ServiceReferenceNode(sr, false));
+                               }
+                       }
+
+                       // SERVICES
+                       ServiceReference[] registeredServices = bundle
+                                       .getRegisteredServices();
+                       if (registeredServices != null) {
+                               for (ServiceReference sr : registeredServices) {
+                                       if (sr != null)
+                                               addChild(new ServiceReferenceNode(sr, true));
+                               }
+                       }
+               }
+
+       }
+
+       Bundle getBundle() {
+               return bundle;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/BundlesView.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/BundlesView.java
new file mode 100644 (file)
index 0000000..c639255
--- /dev/null
@@ -0,0 +1,114 @@
+//package org.argeo.eclipse.ui.workbench.osgi;
+//public class BundlesView {}
+
+package org.argeo.cms.e4.monitoring;
+
+import javax.annotation.PostConstruct;
+
+import org.argeo.eclipse.ui.ColumnViewerComparator;
+import org.argeo.eclipse.ui.specific.EclipseUiSpecificUtils;
+import org.eclipse.e4.ui.di.Focus;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+
+/**
+ * Overview of the bundles as a table. Equivalent to Equinox 'ss' console
+ * command.
+ */
+public class BundlesView {
+       private final static BundleContext bc = FrameworkUtil.getBundle(BundlesView.class).getBundleContext();
+       private TableViewer viewer;
+
+       @PostConstruct
+       public void createPartControl(Composite parent) {
+               viewer = new TableViewer(parent);
+               viewer.setContentProvider(new BundleContentProvider());
+               viewer.getTable().setHeaderVisible(true);
+
+               EclipseUiSpecificUtils.enableToolTipSupport(viewer);
+
+               // ID
+               TableViewerColumn column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(30);
+               column.getColumn().setText("ID");
+               column.getColumn().setAlignment(SWT.RIGHT);
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -3122136344359358605L;
+
+                       public String getText(Object element) {
+                               return Long.toString(((Bundle) element).getBundleId());
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // State
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(18);
+               column.getColumn().setText("State");
+               column.setLabelProvider(new StateLabelProvider());
+               new ColumnViewerComparator(column);
+
+               // Symbolic name
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(250);
+               column.getColumn().setText("Symbolic Name");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -4280840684440451080L;
+
+                       public String getText(Object element) {
+                               return ((Bundle) element).getSymbolicName();
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // Version
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(250);
+               column.getColumn().setText("Version");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = 6871926308708629989L;
+
+                       public String getText(Object element) {
+                               Bundle bundle = (org.osgi.framework.Bundle) element;
+                               return bundle.getVersion().toString();
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               viewer.setInput(bc);
+
+       }
+
+       @Focus
+       public void setFocus() {
+               if (viewer != null)
+                       viewer.getControl().setFocus();
+       }
+
+       /** Content provider managing the array of bundles */
+       private static class BundleContentProvider implements IStructuredContentProvider {
+               private static final long serialVersionUID = -8533792785725875977L;
+
+               public Object[] getElements(Object inputElement) {
+                       if (inputElement instanceof BundleContext) {
+                               BundleContext bc = (BundleContext) inputElement;
+                               return bc.getBundles();
+                       }
+                       return null;
+               }
+
+               public void dispose() {
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/CmsSessionsView.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/CmsSessionsView.java
new file mode 100644 (file)
index 0000000..7200aa9
--- /dev/null
@@ -0,0 +1,173 @@
+//package org.argeo.eclipse.ui.workbench.osgi;
+//public class BundlesView {}
+
+package org.argeo.cms.e4.monitoring;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+
+import org.argeo.api.cms.CmsSession;
+import org.argeo.cms.RoleNameUtils;
+import org.argeo.cms.util.LangUtils;
+import org.argeo.eclipse.ui.ColumnViewerComparator;
+import org.argeo.eclipse.ui.specific.EclipseUiSpecificUtils;
+import org.eclipse.e4.ui.di.Focus;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+
+/**
+ * Overview of the active CMS sessions.
+ */
+public class CmsSessionsView {
+       private final static BundleContext bc = FrameworkUtil.getBundle(CmsSessionsView.class).getBundleContext();
+
+       private TableViewer viewer;
+
+       @PostConstruct
+       public void createPartControl(Composite parent) {
+               viewer = new TableViewer(parent);
+               viewer.setContentProvider(new CmsSessionContentProvider());
+               viewer.getTable().setHeaderVisible(true);
+
+               EclipseUiSpecificUtils.enableToolTipSupport(viewer);
+
+               int longColWidth = 150;
+               int smallColWidth = 100;
+
+               // Display name
+               TableViewerColumn column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(longColWidth);
+               column.getColumn().setText("User");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -5234573509093747505L;
+
+                       public String getText(Object element) {
+                               return ((CmsSession) element).getDisplayName();
+                       }
+
+                       public String getToolTipText(Object element) {
+                               return ((CmsSession) element).getUserDn().toString();
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // Creation time
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(smallColWidth);
+               column.getColumn().setText("Since");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -5234573509093747505L;
+
+                       public String getText(Object element) {
+                               return LangUtils.since(((CmsSession) element).getCreationTime());
+                       }
+
+                       public String getToolTipText(Object element) {
+                               return ((CmsSession) element).getCreationTime().toString();
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // Username
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(smallColWidth);
+               column.getColumn().setText("Username");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -5234573509093747505L;
+
+                       public String getText(Object element) {
+                               String userDn = ((CmsSession) element).getUserDn();
+                               return RoleNameUtils.getLastRdnValue(userDn);
+                       }
+
+                       public String getToolTipText(Object element) {
+                               return ((CmsSession) element).getUserDn().toString();
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // UUID
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(smallColWidth);
+               column.getColumn().setText("UUID");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -5234573509093747505L;
+
+                       public String getText(Object element) {
+                               return ((CmsSession) element).uuid().toString();
+                       }
+
+                       public String getToolTipText(Object element) {
+                               return getText(element);
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // Local ID
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(smallColWidth);
+               column.getColumn().setText("Local ID");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -5234573509093747505L;
+
+                       public String getText(Object element) {
+                               return ((CmsSession) element).getLocalId();
+                       }
+
+                       public String getToolTipText(Object element) {
+                               return getText(element);
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               viewer.setInput(bc);
+
+       }
+
+       @Focus
+       public void setFocus() {
+               if (viewer != null)
+                       viewer.getControl().setFocus();
+       }
+
+       /** Content provider managing the array of bundles */
+       private static class CmsSessionContentProvider implements IStructuredContentProvider {
+               private static final long serialVersionUID = -8533792785725875977L;
+
+               public Object[] getElements(Object inputElement) {
+                       if (inputElement instanceof BundleContext) {
+                               BundleContext bc = (BundleContext) inputElement;
+                               Collection<ServiceReference<CmsSession>> srs;
+                               try {
+                                       srs = bc.getServiceReferences(CmsSession.class, null);
+                               } catch (InvalidSyntaxException e) {
+                                       throw new IllegalArgumentException("Cannot retrieve CMS sessions", e);
+                               }
+                               List<CmsSession> res = new ArrayList<>();
+                               for (ServiceReference<CmsSession> sr : srs) {
+                                       res.add(bc.getService(sr));
+                               }
+                               return res.toArray();
+                       }
+                       return null;
+               }
+
+               public void dispose() {
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/ModulesView.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/ModulesView.java
new file mode 100644 (file)
index 0000000..6317882
--- /dev/null
@@ -0,0 +1,91 @@
+package org.argeo.cms.e4.monitoring;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+
+import org.argeo.cms.ux.widgets.TreeParent;
+import org.eclipse.e4.ui.di.Focus;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+
+/** The OSGi runtime from a module perspective. */
+public class ModulesView {
+       private final static BundleContext bc = FrameworkUtil.getBundle(ModulesView.class).getBundleContext();
+       private TreeViewer viewer;
+
+       @PostConstruct
+       public void createPartControl(Composite parent) {
+               viewer = new TreeViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+               viewer.setContentProvider(new ModulesContentProvider());
+               viewer.setLabelProvider(new ModulesLabelProvider());
+               viewer.setInput(bc);
+       }
+
+       @Focus
+       public void setFocus() {
+               viewer.getTree().setFocus();
+       }
+
+       private class ModulesContentProvider implements ITreeContentProvider {
+               private static final long serialVersionUID = 3819934804640641721L;
+
+               public Object[] getElements(Object inputElement) {
+                       return getChildren(inputElement);
+               }
+
+               public Object[] getChildren(Object parentElement) {
+                       if (parentElement instanceof BundleContext) {
+                               BundleContext bundleContext = (BundleContext) parentElement;
+                               Bundle[] bundles = bundleContext.getBundles();
+
+                               List<BundleNode> modules = new ArrayList<BundleNode>();
+                               for (Bundle bundle : bundles) {
+                                       if (bundle.getState() == Bundle.ACTIVE)
+                                               modules.add(new BundleNode(bundle, true));
+                               }
+                               return modules.toArray();
+                       } else if (parentElement instanceof TreeParent) {
+                               return ((TreeParent) parentElement).getChildren();
+                       } else {
+                               return null;
+                       }
+               }
+
+               public Object getParent(Object element) {
+                       // TODO Auto-generated method stub
+                       return null;
+               }
+
+               public boolean hasChildren(Object element) {
+                       if (element instanceof TreeParent) {
+                               return ((TreeParent) element).hasChildren();
+                       }
+                       return false;
+               }
+
+               public void dispose() {
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+       }
+
+       private class ModulesLabelProvider extends StateLabelProvider {
+               private static final long serialVersionUID = 5290046145534824722L;
+
+               @Override
+               public String getText(Object element) {
+                       if (element instanceof BundleNode)
+                               return element.toString() + " [" + ((BundleNode) element).getBundle().getBundleId() + "]";
+                       return element.toString();
+               }
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/OsgiConfigurationsView.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/OsgiConfigurationsView.java
new file mode 100644 (file)
index 0000000..53e8033
--- /dev/null
@@ -0,0 +1,163 @@
+package org.argeo.cms.e4.monitoring;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Dictionary;
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+
+import org.argeo.cms.swt.CmsException;
+import org.argeo.cms.util.LangUtils;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.TreeViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+
+public class OsgiConfigurationsView {
+       private final static BundleContext bc = FrameworkUtil.getBundle(OsgiConfigurationsView.class).getBundleContext();
+
+       @PostConstruct
+       public void createPartControl(Composite parent) {
+               ConfigurationAdmin configurationAdmin = bc.getService(bc.getServiceReference(ConfigurationAdmin.class));
+
+               TreeViewer viewer = new TreeViewer(parent);
+               // viewer.getTree().setHeaderVisible(true);
+
+               TreeViewerColumn tvc = new TreeViewerColumn(viewer, SWT.NONE);
+               tvc.getColumn().setWidth(400);
+               tvc.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = 835407996597566763L;
+
+                       @Override
+                       public String getText(Object element) {
+                               if (element instanceof Configuration) {
+                                       return ((Configuration) element).getPid();
+                               } else if (element instanceof Prop) {
+                                       return ((Prop) element).key;
+                               }
+                               return super.getText(element);
+                       }
+
+                       @Override
+                       public Image getImage(Object element) {
+                               if (element instanceof Configuration)
+                                       return OsgiExplorerImages.CONFIGURATION;
+                               return null;
+                       }
+
+               });
+
+               tvc = new TreeViewerColumn(viewer, SWT.NONE);
+               tvc.getColumn().setWidth(400);
+               tvc.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = 6999659261190014687L;
+
+                       @Override
+                       public String getText(Object element) {
+                               if (element instanceof Configuration) {
+                                       // return ((Configuration) element).getFactoryPid();
+                                       return null;
+                               } else if (element instanceof Prop) {
+                                       return ((Prop) element).value.toString();
+                               }
+                               return super.getText(element);
+                       }
+               });
+
+               viewer.setContentProvider(new ConfigurationsContentProvider());
+               viewer.setInput(configurationAdmin);
+       }
+
+       static class ConfigurationsContentProvider implements ITreeContentProvider {
+               private static final long serialVersionUID = -4892768279440981042L;
+               private ConfigurationComparator configurationComparator = new ConfigurationComparator();
+
+               @Override
+               public void dispose() {
+               }
+
+               @Override
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+
+               @Override
+               public Object[] getElements(Object inputElement) {
+                       ConfigurationAdmin configurationAdmin = (ConfigurationAdmin) inputElement;
+                       try {
+                               Configuration[] configurations = configurationAdmin.listConfigurations(null);
+                               Arrays.sort(configurations, configurationComparator);
+                               return configurations;
+                       } catch (IOException | InvalidSyntaxException e) {
+                               throw new CmsException("Cannot list configurations", e);
+                       }
+               }
+
+               @Override
+               public Object[] getChildren(Object parentElement) {
+                       if (parentElement instanceof Configuration) {
+                               List<Prop> res = new ArrayList<>();
+                               Configuration configuration = (Configuration) parentElement;
+                               Dictionary<String, Object> props = configuration.getProperties();
+                               keys: for (String key : LangUtils.keys(props)) {
+                                       if (Constants.SERVICE_PID.equals(key))
+                                               continue keys;
+                                       if (ConfigurationAdmin.SERVICE_FACTORYPID.equals(key))
+                                               continue keys;
+                                       res.add(new Prop(configuration, key, props.get(key)));
+                               }
+                               return res.toArray(new Prop[res.size()]);
+                       }
+                       return null;
+               }
+
+               @Override
+               public Object getParent(Object element) {
+                       if (element instanceof Prop)
+                               return ((Prop) element).configuration;
+                       return null;
+               }
+
+               @Override
+               public boolean hasChildren(Object element) {
+                       if (element instanceof Configuration)
+                               return true;
+                       return false;
+               }
+
+       }
+
+       static class Prop {
+               final Configuration configuration;
+               final String key;
+               final Object value;
+
+               public Prop(Configuration configuration, String key, Object value) {
+                       this.configuration = configuration;
+                       this.key = key;
+                       this.value = value;
+               }
+
+       }
+
+       static class ConfigurationComparator implements Comparator<Configuration> {
+
+               @Override
+               public int compare(Configuration o1, Configuration o2) {
+                       return o1.getPid().compareTo(o2.getPid());
+               }
+
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/OsgiExplorerImages.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/OsgiExplorerImages.java
new file mode 100644 (file)
index 0000000..7217fe6
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.cms.e4.monitoring;
+
+import org.argeo.cms.ui.theme.CmsImages;
+import org.eclipse.swt.graphics.Image;
+
+/** Shared icons. */
+public class OsgiExplorerImages extends CmsImages {
+       public final static Image INSTALLED = createIcon("installed.gif");
+       public final static Image RESOLVED = createIcon("resolved.gif");
+       public final static Image STARTING = createIcon("starting.gif");
+       public final static Image ACTIVE = createIcon("active.gif");
+       public final static Image SERVICE_PUBLISHED = createIcon("service_published.gif");
+       public final static Image SERVICE_REFERENCED = createIcon("service_referenced.gif");
+       public final static Image CONFIGURATION = createIcon("node.gif");
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/ServiceReferenceNode.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/ServiceReferenceNode.java
new file mode 100644 (file)
index 0000000..1c60811
--- /dev/null
@@ -0,0 +1,46 @@
+package org.argeo.cms.e4.monitoring;
+
+import org.argeo.cms.ux.widgets.TreeParent;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.ServiceReference;
+
+/** A tree element representing a {@link ServiceReference} */
+@SuppressWarnings({ "rawtypes" })
+class ServiceReferenceNode extends TreeParent {
+       private final ServiceReference serviceReference;
+       private final boolean published;
+
+       public ServiceReferenceNode(ServiceReference serviceReference,
+                       boolean published) {
+               super(serviceReference.toString());
+               this.serviceReference = serviceReference;
+               this.published = published;
+
+               if (isPublished()) {
+                       Bundle[] usedBundles = serviceReference.getUsingBundles();
+                       if (usedBundles != null) {
+                               for (Bundle b : usedBundles) {
+                                       if (b != null)
+                                               addChild(new BundleNode(b));
+                               }
+                       }
+               } else {
+                       Bundle provider = serviceReference.getBundle();
+                       addChild(new BundleNode(provider));
+               }
+
+               for (String key : serviceReference.getPropertyKeys()) {
+                       addChild(new TreeParent(key + "="
+                                       + serviceReference.getProperty(key)));
+               }
+
+       }
+
+       public ServiceReference getServiceReference() {
+               return serviceReference;
+       }
+
+       public boolean isPublished() {
+               return published;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/StateLabelProvider.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/StateLabelProvider.java
new file mode 100644 (file)
index 0000000..5cb5b65
--- /dev/null
@@ -0,0 +1,82 @@
+package org.argeo.cms.e4.monitoring;
+
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.graphics.Image;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+
+/** Label provider showing the sate of bundles */
+class StateLabelProvider extends ColumnLabelProvider {
+       private static final long serialVersionUID = -7885583135316000733L;
+
+       @Override
+       public Image getImage(Object element) {
+               int state;
+               if (element instanceof Bundle)
+                       state = ((Bundle) element).getState();
+               else if (element instanceof BundleNode)
+                       state = ((BundleNode) element).getBundle().getState();
+               else if (element instanceof ServiceReferenceNode)
+                       if (((ServiceReferenceNode) element).isPublished())
+                               return OsgiExplorerImages.SERVICE_PUBLISHED;
+                       else
+                               return OsgiExplorerImages.SERVICE_REFERENCED;
+               else
+                       return null;
+
+               switch (state) {
+               case Bundle.UNINSTALLED:
+                       return OsgiExplorerImages.INSTALLED;
+               case Bundle.INSTALLED:
+                       return OsgiExplorerImages.INSTALLED;
+               case Bundle.RESOLVED:
+                       return OsgiExplorerImages.RESOLVED;
+               case Bundle.STARTING:
+                       return OsgiExplorerImages.STARTING;
+               case Bundle.STOPPING:
+                       return OsgiExplorerImages.STARTING;
+               case Bundle.ACTIVE:
+                       return OsgiExplorerImages.ACTIVE;
+               default:
+                       return null;
+               }
+       }
+
+       @Override
+       public String getText(Object element) {
+               return null;
+       }
+
+       @Override
+       public String getToolTipText(Object element) {
+               Bundle bundle = (Bundle) element;
+               Integer state = bundle.getState();
+               switch (state) {
+               case Bundle.UNINSTALLED:
+                       return "UNINSTALLED";
+               case Bundle.INSTALLED:
+                       return "INSTALLED";
+               case Bundle.RESOLVED:
+                       return "RESOLVED";
+               case Bundle.STARTING:
+                       String activationPolicy = bundle.getHeaders()
+                                       .get(Constants.BUNDLE_ACTIVATIONPOLICY).toString();
+
+                       // .get("Bundle-ActivationPolicy").toString();
+                       // FIXME constant triggers the compilation failure
+                       if (activationPolicy != null
+                                       && activationPolicy.equals(Constants.ACTIVATION_LAZY))
+                               // && activationPolicy.equals("lazy"))
+                               // FIXME constant triggers the compilation failure
+                               // && activationPolicy.equals(Constants.ACTIVATION_LAZY))
+                               return "<<LAZY>>";
+                       return "STARTING";
+               case Bundle.STOPPING:
+                       return "STOPPING";
+               case Bundle.ACTIVE:
+                       return "ACTIVE";
+               default:
+                       return null;
+               }
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/package-info.java
new file mode 100644 (file)
index 0000000..873bf31
--- /dev/null
@@ -0,0 +1,2 @@
+/** Monitoring perspective. */
+package org.argeo.cms.e4.monitoring;
\ No newline at end of file
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/parts/EgoDashboard.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/parts/EgoDashboard.java
new file mode 100644 (file)
index 0000000..79e2591
--- /dev/null
@@ -0,0 +1,41 @@
+package org.argeo.cms.e4.parts;
+
+import java.time.ZonedDateTime;
+
+import javax.annotation.PostConstruct;
+
+import org.argeo.api.cms.CmsSession;
+import org.argeo.cms.CurrentUser;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+
+/** A canonical view of the logged in user. */
+public class EgoDashboard {
+//     private BundleContext bc = FrameworkUtil.getBundle(EgoDashboard.class).getBundleContext();
+
+       @PostConstruct
+       public void createPartControl(Composite p) {
+               p.setLayout(new GridLayout());
+               String username = CurrentUser.getUsername();
+
+               CmsSwtUtils.lbl(p, "<strong>" + CurrentUser.getDisplayName() + "</strong>");
+               CmsSwtUtils.txt(p, username);
+               CmsSwtUtils.lbl(p, "Roles:");
+               roles: for (String role : CurrentUser.roles()) {
+                       if (username.equals(role))
+                               continue roles;
+                       CmsSwtUtils.txt(p, role);
+               }
+
+//             Subject subject = Subject.getSubject(AccessController.getContext());
+//             if (subject != null) {
+               CmsSession cmsSession = CurrentUser.getCmsSession();
+               ZonedDateTime loggedIndSince = cmsSession.getCreationTime();
+               CmsSwtUtils.lbl(p, "Session:");
+               CmsSwtUtils.txt(p, cmsSession.uuid().toString());
+               CmsSwtUtils.lbl(p, "Logged in since:");
+               CmsSwtUtils.txt(p, loggedIndSince.toString());
+//             }
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/AbstractRoleEditor.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/AbstractRoleEditor.java
new file mode 100644 (file)
index 0000000..e3ec913
--- /dev/null
@@ -0,0 +1,287 @@
+package org.argeo.cms.e4.users;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+
+import org.argeo.api.acr.ldap.LdapAttr;
+import org.argeo.cms.auth.UserAdminUtils;
+import org.argeo.cms.ui.eclipse.forms.AbstractFormPart;
+import org.argeo.cms.ui.eclipse.forms.IManagedForm;
+import org.argeo.cms.ui.eclipse.forms.ManagedForm;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.e4.ui.di.Persist;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.osgi.service.useradmin.Authorization;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Editor for a user, might be a user or a group. */
+public abstract class AbstractRoleEditor {
+
+       // public final static String USER_EDITOR_ID = WorkbenchUiPlugin.PLUGIN_ID +
+       // ".userEditor";
+       // public final static String GROUP_EDITOR_ID = WorkbenchUiPlugin.PLUGIN_ID +
+       // ".groupEditor";
+
+       /* DEPENDENCY INJECTION */
+       @Inject
+       protected UserAdminWrapper userAdminWrapper;
+
+       @Inject
+       private MPart mPart;
+
+       // @Inject
+       // Composite parent;
+
+       private UserAdmin userAdmin;
+
+       // Context
+       private User user;
+       private String username;
+
+       private NameChangeListener listener;
+
+       private ManagedForm managedForm;
+
+       // public void init(IEditorSite site, IEditorInput input) throws
+       // PartInitException {
+       @PostConstruct
+       public void init(Composite parent) {
+               this.userAdmin = userAdminWrapper.getUserAdmin();
+               username = mPart.getPersistedState().get(LdapAttr.uid.name());
+               user = (User) userAdmin.getRole(username);
+
+               listener = new NameChangeListener(Display.getCurrent());
+               userAdminWrapper.addListener(listener);
+               updateEditorTitle(null);
+
+               managedForm = new ManagedForm(parent) {
+
+                       @Override
+                       public void staleStateChanged() {
+                               refresh();
+                       }
+               };
+               ScrolledComposite scrolled = managedForm.getForm();
+               Composite body = new Composite(scrolled, SWT.NONE);
+               scrolled.setContent(body);
+               createUi(body);
+               managedForm.refresh();
+       }
+
+       abstract void createUi(Composite parent);
+
+       /**
+        * returns the list of all authorizations for the given user or of the current
+        * displayed user if parameter is null
+        */
+       protected List<User> getFlatGroups(User aUser) {
+               Authorization currAuth;
+               if (aUser == null)
+                       currAuth = userAdmin.getAuthorization(this.user);
+               else
+                       currAuth = userAdmin.getAuthorization(aUser);
+
+               String[] roles = currAuth.getRoles();
+
+               List<User> groups = new ArrayList<User>();
+               for (String roleStr : roles) {
+                       User currRole = (User) userAdmin.getRole(roleStr);
+                       if (currRole != null && !groups.contains(currRole))
+                               groups.add(currRole);
+               }
+               return groups;
+       }
+
+       protected IManagedForm getManagedForm() {
+               return managedForm;
+       }
+
+       /** Exposes the user (or group) that is displayed by the current editor */
+       protected User getDisplayedUser() {
+               return user;
+       }
+
+       private void setDisplayedUser(User user) {
+               this.user = user;
+       }
+
+       void updateEditorTitle(String title) {
+               if (title == null) {
+                       String commonName = UserAdminUtils.getProperty(user, LdapAttr.cn.name());
+                       title = "".equals(commonName) ? user.getName() : commonName;
+               }
+               setPartName(title);
+       }
+
+       protected void setPartName(String name) {
+               mPart.setLabel(name);
+       }
+
+       // protected void addPages() {
+       // try {
+       // if (user.getType() == Role.GROUP)
+       // addPage(new GroupMainPage(this, userAdminWrapper, repository, nodeInstance));
+       // else
+       // addPage(new UserMainPage(this, userAdminWrapper));
+       // } catch (Exception e) {
+       // throw new CmsException("Cannot add pages", e);
+       // }
+       // }
+
+       @Persist
+       public void doSave(IProgressMonitor monitor) {
+               userAdminWrapper.beginTransactionIfNeeded();
+               commitPages(true);
+               userAdminWrapper.commitOrNotifyTransactionStateChange();
+               // firePropertyChange(PROP_DIRTY);
+               userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_REMOVED, user));
+       }
+
+       protected void commitPages(boolean b) {
+               managedForm.commit(b);
+       }
+
+       @PreDestroy
+       public void dispose() {
+               userAdminWrapper.removeListener(listener);
+               managedForm.dispose();
+       }
+
+       // CONTROLERS FOR THIS EDITOR AND ITS PAGES
+
+       class NameChangeListener extends UiUserAdminListener {
+               public NameChangeListener(Display display) {
+                       super(display);
+               }
+
+               @Override
+               public void roleChangedToUiThread(UserAdminEvent event) {
+                       Role changedRole = event.getRole();
+                       if (changedRole == null || changedRole.equals(user)) {
+                               updateEditorTitle(null);
+                               User reloadedUser = (User) userAdminWrapper.getUserAdmin().getRole(user.getName());
+                               setDisplayedUser(reloadedUser);
+                       }
+               }
+       }
+
+       class MainInfoListener extends UiUserAdminListener {
+               private final AbstractFormPart part;
+
+               public MainInfoListener(Display display, AbstractFormPart part) {
+                       super(display);
+                       this.part = part;
+               }
+
+               @Override
+               public void roleChangedToUiThread(UserAdminEvent event) {
+                       // Rollback
+                       if (event.getRole() == null)
+                               part.markStale();
+               }
+       }
+
+       class GroupChangeListener extends UiUserAdminListener {
+               private final AbstractFormPart part;
+
+               public GroupChangeListener(Display display, AbstractFormPart part) {
+                       super(display);
+                       this.part = part;
+               }
+
+               @Override
+               public void roleChangedToUiThread(UserAdminEvent event) {
+                       // always mark as stale
+                       part.markStale();
+               }
+       }
+
+       /** Registers a listener that will notify this part */
+       class FormPartML implements ModifyListener {
+               private static final long serialVersionUID = 6299808129505381333L;
+               private AbstractFormPart formPart;
+
+               public FormPartML(AbstractFormPart generalPart) {
+                       this.formPart = generalPart;
+               }
+
+               public void modifyText(ModifyEvent e) {
+                       // Discard event when the control does not have the focus, typically
+                       // to avoid all editors being marked as dirty during a Rollback
+                       if (((Control) e.widget).isFocusControl())
+                               formPart.markDirty();
+               }
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+
+       /** Creates label and multiline text. */
+       Text createLMT(Composite parent, String label, String value) {
+               Label lbl = new Label(parent, SWT.NONE);
+               lbl.setText(label);
+               lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
+               Text text = new Text(parent, SWT.NONE);
+               text.setText(value);
+               text.setLayoutData(new GridData(SWT.LEAD, SWT.FILL, true, true));
+               return text;
+       }
+
+       /** Creates label and password. */
+       Text createLP(Composite parent, String label, String value) {
+               Label lbl = new Label(parent, SWT.NONE);
+               lbl.setText(label);
+               lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
+               Text text = new Text(parent, SWT.PASSWORD | SWT.BORDER);
+               text.setText(value);
+               text.setLayoutData(new GridData(SWT.LEAD, SWT.FILL, true, false));
+               return text;
+       }
+
+       /** Creates label and text. */
+       Text createLT(Composite parent, String label, String value) {
+               Label lbl = new Label(parent, SWT.NONE);
+               lbl.setText(label);
+               lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
+               lbl.setFont(EclipseUiUtils.getBoldFont(parent));
+               Text text = new Text(parent, SWT.BORDER);
+               text.setText(value);
+               text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+               // CmsUiUtils.style(text, CmsWorkbenchStyles.WORKBENCH_FORM_TEXT);
+               return text;
+       }
+
+       Text createReadOnlyLT(Composite parent, String label, String value) {
+               Label lbl = new Label(parent, SWT.NONE);
+               lbl.setText(label);
+               lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
+               lbl.setFont(EclipseUiUtils.getBoldFont(parent));
+               Text text = new Text(parent, SWT.NONE);
+               text.setText(value);
+               text.setLayoutData(new GridData(SWT.LEAD, SWT.FILL, true, false));
+               text.setEditable(false);
+               // CmsUiUtils.style(text, CmsWorkbenchStyles.WORKBENCH_FORM_TEXT);
+               return text;
+       }
+
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/CmsWorkbenchStyles.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/CmsWorkbenchStyles.java
new file mode 100644 (file)
index 0000000..07df312
--- /dev/null
@@ -0,0 +1,8 @@
+package org.argeo.cms.e4.users;
+
+/** Centralize the declaration of Workbench specific CSS Styles */
+interface CmsWorkbenchStyles {
+
+       // Specific People layouting
+       String WORKBENCH_FORM_TEXT = "workbench_form_text";
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/GroupEditor.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/GroupEditor.java
new file mode 100644 (file)
index 0000000..8ec1621
--- /dev/null
@@ -0,0 +1,566 @@
+package org.argeo.cms.e4.users;
+
+import static org.argeo.api.acr.ldap.LdapAttr.businessCategory;
+import static org.argeo.api.acr.ldap.LdapAttr.description;
+import static org.argeo.api.cms.CmsContext.WORKGROUP;
+import static org.argeo.cms.auth.UserAdminUtils.setProperty;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.api.acr.ldap.LdapAttr;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.CmsContext;
+import org.argeo.api.cms.transaction.WorkTransaction;
+import org.argeo.cms.auth.UserAdminUtils;
+import org.argeo.cms.e4.users.providers.CommonNameLP;
+import org.argeo.cms.e4.users.providers.MailLP;
+import org.argeo.cms.e4.users.providers.RoleIconLP;
+import org.argeo.cms.e4.users.providers.UserFilter;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.swt.useradmin.LdifUsersTable;
+import org.argeo.cms.ui.eclipse.forms.AbstractFormPart;
+import org.argeo.cms.ui.eclipse.forms.IManagedForm;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.jcr.JcrException;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.ViewerDropAdapter;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.ToolBar;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+//import org.eclipse.ui.forms.AbstractFormPart;
+//import org.eclipse.ui.forms.IManagedForm;
+//import org.eclipse.ui.forms.SectionPart;
+//import org.eclipse.ui.forms.editor.FormEditor;
+//import org.eclipse.ui.forms.editor.FormPage;
+//import org.eclipse.ui.forms.widgets.FormToolkit;
+//import org.eclipse.ui.forms.widgets.ScrolledForm;
+//import org.eclipse.ui.forms.widgets.Section;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Display/edit main properties of a given group */
+public class GroupEditor extends AbstractRoleEditor {
+       // final static String ID = "GroupEditor.mainPage";
+
+       @Inject
+       private EPartService partService;
+
+       // private final UserEditor editor;
+       @Inject
+       private Repository repository;
+       @Inject
+       private CmsContext nodeInstance;
+       // private final UserAdminWrapper userAdminWrapper;
+       private Session groupsSession;
+
+       // public GroupMainPage(FormEditor editor, UserAdminWrapper userAdminWrapper,
+       // Repository repository,
+       // NodeInstance nodeInstance) {
+       // super(editor, ID, "Main");
+       // try {
+       // session = repository.login();
+       // } catch (RepositoryException e) {
+       // throw new CmsException("Cannot retrieve session of in MainGroupPage
+       // constructor", e);
+       // }
+       // this.editor = (UserEditor) editor;
+       // this.userAdminWrapper = userAdminWrapper;
+       // this.nodeInstance = nodeInstance;
+       // }
+
+       // protected void createFormContent(final IManagedForm mf) {
+       // ScrolledForm form = mf.getForm();
+       // Composite body = form.getBody();
+       // GridLayout mainLayout = new GridLayout();
+       // body.setLayout(mainLayout);
+       // Group group = (Group) editor.getDisplayedUser();
+       // appendOverviewPart(body, group);
+       // appendMembersPart(body, group);
+       // }
+
+       @Override
+       protected void createUi(Composite parent) {
+               try {
+                       groupsSession = repository.login(CmsConstants.SRV_WORKSPACE);
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot retrieve session", e);
+               }
+               // ScrolledForm form = mf.getForm();
+               // Composite body = form.getBody();
+               // Composite body = new Composite(parent, SWT.NONE);
+               Composite body = parent;
+               GridLayout mainLayout = new GridLayout();
+               body.setLayout(mainLayout);
+               Group group = (Group) getDisplayedUser();
+               appendOverviewPart(body, group);
+               appendMembersPart(body, group);
+       }
+
+       @PreDestroy
+       public void dispose() {
+               JcrUtils.logoutQuietly(groupsSession);
+               super.dispose();
+       }
+
+       /** Creates the general section */
+       protected void appendOverviewPart(final Composite parent, final Group group) {
+               Composite body = new Composite(parent, SWT.NONE);
+               // GridLayout layout = new GridLayout(5, false);
+               GridLayout layout = new GridLayout(2, false);
+               body.setLayout(layout);
+               body.setLayoutData(CmsSwtUtils.fillWidth());
+
+               String cn = UserAdminUtils.getProperty(group, LdapAttr.cn.name());
+               createReadOnlyLT(body, "Name", cn);
+               createReadOnlyLT(body, "DN", group.getName());
+               createReadOnlyLT(body, "Domain", UserAdminUtils.getDomainName(group));
+
+               // Description
+               Label descLbl = new Label(body, SWT.LEAD);
+               descLbl.setFont(EclipseUiUtils.getBoldFont(body));
+               descLbl.setText("Description");
+               descLbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, true, false, 2, 1));
+               final Text descTxt = new Text(body, SWT.LEAD | SWT.MULTI | SWT.WRAP | SWT.BORDER);
+               GridData gd = EclipseUiUtils.fillWidth();
+               gd.heightHint = 50;
+               gd.horizontalSpan = 2;
+               descTxt.setLayoutData(gd);
+
+               // Mark as workgroup
+               Link markAsWorkgroupLk = new Link(body, SWT.NONE);
+               markAsWorkgroupLk.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 2, 1));
+
+               // create form part (controller)
+               final AbstractFormPart part = new AbstractFormPart() {
+
+                       private MainInfoListener listener;
+
+                       @Override
+                       public void initialize(IManagedForm form) {
+                               super.initialize(form);
+                               listener = new MainInfoListener(parent.getDisplay(), this);
+                               userAdminWrapper.addListener(listener);
+                       }
+
+                       @Override
+                       public void dispose() {
+                               userAdminWrapper.removeListener(listener);
+                               super.dispose();
+                       }
+
+                       public void commit(boolean onSave) {
+                               // group.getProperties().put(LdapAttrs.description.name(), descTxt.getText());
+                               setProperty(group, description, descTxt.getText());
+                               super.commit(onSave);
+                       }
+
+                       @Override
+                       public void refresh() {
+                               // dnTxt.setText(group.getName());
+                               // cnTxt.setText(UserAdminUtils.getProperty(group, LdapAttrs.cn.name()));
+                               descTxt.setText(UserAdminUtils.getProperty(group, LdapAttr.description.name()));
+                               Node workgroupHome = CmsJcrUtils.getGroupHome(groupsSession, cn);
+                               if (workgroupHome == null)
+                                       markAsWorkgroupLk.setText("<a>Mark as workgroup</a>");
+                               else
+                                       markAsWorkgroupLk.setText("Configured as workgroup");
+                               parent.layout(true, true);
+                               super.refresh();
+                       }
+               };
+
+               markAsWorkgroupLk.addSelectionListener(new SelectionAdapter() {
+                       private static final long serialVersionUID = -6439340898096365078L;
+
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+
+                               boolean confirmed = MessageDialog.openConfirm(parent.getShell(), "Mark as workgroup",
+                                               "Are you sure you want to mark " + cn + " as being a workgroup? ");
+                               if (confirmed) {
+                                       Node workgroupHome = CmsJcrUtils.getGroupHome(groupsSession, cn);
+                                       if (workgroupHome != null)
+                                               return; // already marked as workgroup, do nothing
+                                       else {
+                                               // improve transaction management
+                                               userAdminWrapper.beginTransactionIfNeeded();
+                                               nodeInstance.createWorkgroup(group.getName());
+                                               setProperty(group, businessCategory, WORKGROUP);
+                                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                                               userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, group));
+                                               part.refresh();
+                                       }
+                               }
+                       }
+               });
+
+               ModifyListener defaultListener = new FormPartML(part);
+               descTxt.addModifyListener(defaultListener);
+               getManagedForm().addPart(part);
+       }
+
+       /** Filtered table with members. Has drag and drop ability */
+       protected void appendMembersPart(Composite parent, Group group) {
+               // Section section = tk.createSection(parent, Section.TITLE_BAR);
+               // section.setText("Members");
+               // section.setLayoutData(EclipseUiUtils.fillAll());
+
+               Composite body = new Composite(parent, SWT.BORDER);
+               body.setLayout(new GridLayout());
+               // section.setClient(body);
+               body.setLayoutData(EclipseUiUtils.fillAll());
+
+               // Define the displayed columns
+               List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+               columnDefs.add(new ColumnDefinition(new RoleIconLP(), "", 0, 24));
+               columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Name", 150));
+               columnDefs.add(new ColumnDefinition(new MailLP(), "Mail", 150));
+               // columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name",
+               // 240));
+
+               // Create and configure the table
+               LdifUsersTable userViewerCmp = new MyUserTableViewer(body, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL,
+                               userAdminWrapper.getUserAdmin());
+
+               userViewerCmp.setColumnDefinitions(columnDefs);
+               userViewerCmp.populate(true, false);
+               userViewerCmp.setLayoutData(EclipseUiUtils.fillAll());
+
+               // Controllers
+               TableViewer userViewer = userViewerCmp.getTableViewer();
+               userViewer.addDoubleClickListener(new UserTableDefaultDClickListener(partService));
+               int operations = DND.DROP_COPY | DND.DROP_MOVE;
+               Transfer[] tt = new Transfer[] { TextTransfer.getInstance() };
+               userViewer.addDropSupport(operations, tt,
+                               new GroupDropListener(userAdminWrapper, userViewerCmp, (Group) getDisplayedUser()));
+
+               AbstractFormPart part = new GroupMembersPart(userViewerCmp);
+               getManagedForm().addPart(part);
+
+               // remove button
+               // addRemoveAbility(toolBarManager, userViewerCmp.getTableViewer(), group);
+               Action action = new RemoveMembershipAction(userViewer, group, "Remove selected items from this group",
+                               SecurityAdminImages.ICON_REMOVE_DESC);
+
+               ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
+               ToolBar toolBar = toolBarManager.createControl(body);
+               toolBar.setLayoutData(CmsSwtUtils.fillWidth());
+
+               toolBarManager.add(action);
+               toolBarManager.update(true);
+
+       }
+
+       // private LdifUsersTable createMemberPart(Composite parent, Group group) {
+       //
+       // // Define the displayed columns
+       // List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+       // columnDefs.add(new ColumnDefinition(new RoleIconLP(), "", 0, 24));
+       // columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Name", 150));
+       // columnDefs.add(new ColumnDefinition(new MailLP(), "Mail", 150));
+       // // columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished
+       // Name",
+       // // 240));
+       //
+       // // Create and configure the table
+       // LdifUsersTable userViewerCmp = new MyUserTableViewer(parent, SWT.MULTI |
+       // SWT.H_SCROLL | SWT.V_SCROLL,
+       // userAdminWrapper.getUserAdmin());
+       //
+       // userViewerCmp.setColumnDefinitions(columnDefs);
+       // userViewerCmp.populate(true, false);
+       // userViewerCmp.setLayoutData(EclipseUiUtils.fillAll());
+       //
+       // // Controllers
+       // TableViewer userViewer = userViewerCmp.getTableViewer();
+       // userViewer.addDoubleClickListener(new
+       // UserTableDefaultDClickListener(partService));
+       // int operations = DND.DROP_COPY | DND.DROP_MOVE;
+       // Transfer[] tt = new Transfer[] { TextTransfer.getInstance() };
+       // userViewer.addDropSupport(operations, tt,
+       // new GroupDropListener(userAdminWrapper, userViewerCmp, (Group)
+       // getDisplayedUser()));
+       //
+       // // userViewerCmp.refresh();
+       // return userViewerCmp;
+       // }
+
+       // Local viewers
+       private class MyUserTableViewer extends LdifUsersTable {
+               private static final long serialVersionUID = 8467999509931900367L;
+
+               private final UserFilter userFilter;
+
+               public MyUserTableViewer(Composite parent, int style, UserAdmin userAdmin) {
+                       super(parent, style, true);
+                       userFilter = new UserFilter();
+
+               }
+
+               @Override
+               protected List<User> listFilteredElements(String filter) {
+                       // reload user and set it in the editor
+                       Group group = (Group) getDisplayedUser();
+                       Role[] roles = group.getMembers();
+                       List<User> users = new ArrayList<User>();
+                       userFilter.setSearchText(filter);
+                       // userFilter.setShowSystemRole(true);
+                       for (Role role : roles)
+                               // if (role.getType() == Role.GROUP)
+                               if (userFilter.select(null, null, role))
+                                       users.add((User) role);
+                       return users;
+               }
+       }
+
+       // private void addRemoveAbility(ToolBarManager toolBarManager, TableViewer
+       // userViewer, Group group) {
+       // // Section section = sectionPart.getSection();
+       // // ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
+       // // ToolBar toolbar = toolBarManager.createControl(parent);
+       // // ToolBar toolbar = toolBarManager.getControl();
+       // // final Cursor handCursor = new Cursor(toolbar.getDisplay(),
+       // SWT.CURSOR_HAND);
+       // // toolbar.setCursor(handCursor);
+       // // toolbar.addDisposeListener(new DisposeListener() {
+       // // private static final long serialVersionUID = 3882131405820522925L;
+       // //
+       // // public void widgetDisposed(DisposeEvent e) {
+       // // if ((handCursor != null) && (handCursor.isDisposed() == false)) {
+       // // handCursor.dispose();
+       // // }
+       // // }
+       // // });
+       //
+       // Action action = new RemoveMembershipAction(userViewer, group, "Remove
+       // selected items from this group",
+       // SecurityAdminImages.ICON_REMOVE_DESC);
+       // toolBarManager.add(action);
+       // toolBarManager.update(true);
+       // // section.setTextClient(toolbar);
+       // }
+
+       private class RemoveMembershipAction extends Action {
+               private static final long serialVersionUID = -1337713097184522588L;
+
+               private final TableViewer userViewer;
+               private final Group group;
+
+               RemoveMembershipAction(TableViewer userViewer, Group group, String name, ImageDescriptor img) {
+                       super(name, img);
+                       this.userViewer = userViewer;
+                       this.group = group;
+               }
+
+               @Override
+               public void run() {
+                       ISelection selection = userViewer.getSelection();
+                       if (selection.isEmpty())
+                               return;
+
+                       @SuppressWarnings("unchecked")
+                       Iterator<User> it = ((IStructuredSelection) selection).iterator();
+                       List<User> users = new ArrayList<User>();
+                       while (it.hasNext()) {
+                               User currUser = it.next();
+                               users.add(currUser);
+                       }
+
+                       userAdminWrapper.beginTransactionIfNeeded();
+                       for (User user : users) {
+                               group.removeMember(user);
+                       }
+                       userAdminWrapper.commitOrNotifyTransactionStateChange();
+                       userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, group));
+               }
+       }
+
+       // LOCAL CONTROLLERS
+       private class GroupMembersPart extends AbstractFormPart {
+               private final LdifUsersTable userViewer;
+               // private final Group group;
+
+               private GroupChangeListener listener;
+
+               public GroupMembersPart(LdifUsersTable userViewer) {
+                       // super(section);
+                       this.userViewer = userViewer;
+                       // this.group = group;
+               }
+
+               @Override
+               public void initialize(IManagedForm form) {
+                       super.initialize(form);
+                       listener = new GroupChangeListener(userViewer.getDisplay(), GroupMembersPart.this);
+                       userAdminWrapper.addListener(listener);
+               }
+
+               @Override
+               public void dispose() {
+                       userAdminWrapper.removeListener(listener);
+                       super.dispose();
+               }
+
+               @Override
+               public void refresh() {
+                       userViewer.refresh();
+                       super.refresh();
+               }
+       }
+
+       /**
+        * Defines this table as being a potential target to add group membership
+        * (roles) to this group
+        */
+       private class GroupDropListener extends ViewerDropAdapter {
+               private static final long serialVersionUID = 2893468717831451621L;
+
+               private final UserAdminWrapper userAdminWrapper;
+               // private final LdifUsersTable myUserViewerCmp;
+               private final Group myGroup;
+
+               public GroupDropListener(UserAdminWrapper userAdminWrapper, LdifUsersTable userTableViewerCmp, Group group) {
+                       super(userTableViewerCmp.getTableViewer());
+                       this.userAdminWrapper = userAdminWrapper;
+                       this.myGroup = group;
+                       // this.myUserViewerCmp = userTableViewerCmp;
+               }
+
+               @Override
+               public boolean validateDrop(Object target, int operation, TransferData transferType) {
+                       // Target is always OK in a list only view
+                       // TODO check if not a string
+                       boolean validDrop = true;
+                       return validDrop;
+               }
+
+               @Override
+               public void drop(DropTargetEvent event) {
+                       // TODO Is there an opportunity to perform the check before?
+                       String newUserName = (String) event.data;
+                       UserAdmin myUserAdmin = userAdminWrapper.getUserAdmin();
+                       Role role = myUserAdmin.getRole(newUserName);
+                       if (role.getType() == Role.GROUP) {
+                               Group newGroup = (Group) role;
+                               Shell shell = getViewer().getControl().getShell();
+                               // Sanity checks
+                               if (myGroup == newGroup) { // Equality
+                                       MessageDialog.openError(shell, "Forbidden addition ", "A group cannot be a member of itself.");
+                                       return;
+                               }
+
+                               // Cycle
+                               String myName = myGroup.getName();
+                               List<User> myMemberships = getFlatGroups(myGroup);
+                               if (myMemberships.contains(newGroup)) {
+                                       MessageDialog.openError(shell, "Forbidden addition: cycle",
+                                                       "Cannot add " + newUserName + " to group " + myName + ". This would create a cycle");
+                                       return;
+                               }
+
+                               // Already member
+                               List<User> newGroupMemberships = getFlatGroups(newGroup);
+                               if (newGroupMemberships.contains(myGroup)) {
+                                       MessageDialog.openError(shell, "Forbidden addition",
+                                                       "Cannot add " + newUserName + " to group " + myName + ", this membership already exists");
+                                       return;
+                               }
+                               userAdminWrapper.beginTransactionIfNeeded();
+                               myGroup.addMember(newGroup);
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, myGroup));
+                       } else if (role.getType() == Role.USER) {
+                               // TODO check if the group is already member of this group
+                               WorkTransaction transaction = userAdminWrapper.beginTransactionIfNeeded();
+                               User user = (User) role;
+                               myGroup.addMember(user);
+                               if (UserAdminWrapper.COMMIT_ON_SAVE)
+                                       try {
+                                               transaction.commit();
+                                       } catch (Exception e) {
+                                               throw new IllegalStateException(
+                                                               "Cannot commit transaction " + "after user group membership update", e);
+                                       }
+                               userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, myGroup));
+                       }
+                       super.drop(event);
+               }
+
+               @Override
+               public boolean performDrop(Object data) {
+                       // myUserViewerCmp.refresh();
+                       return true;
+               }
+       }
+
+       // LOCAL HELPERS
+       // private Composite addSection(FormToolkit tk, Composite parent) {
+       // Section section = tk.createSection(parent, SWT.NO_FOCUS);
+       // section.setLayoutData(EclipseUiUtils.fillWidth());
+       // Composite body = tk.createComposite(section, SWT.WRAP);
+       // body.setLayoutData(EclipseUiUtils.fillAll());
+       // section.setClient(body);
+       // return body;
+       // }
+
+       /** Creates label and text. */
+       // private Text createLT(Composite parent, String label, String value) {
+       // FormToolkit toolkit = getManagedForm().getToolkit();
+       // Label lbl = toolkit.createLabel(parent, label);
+       // lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
+       // lbl.setFont(EclipseUiUtils.getBoldFont(parent));
+       // Text text = toolkit.createText(parent, value, SWT.BORDER);
+       // text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+       // CmsUiUtils.style(text, CmsWorkbenchStyles.WORKBENCH_FORM_TEXT);
+       // return text;
+       // }
+       //
+       // Text createReadOnlyLT(Composite parent, String label, String value) {
+       // FormToolkit toolkit = getManagedForm().getToolkit();
+       // Label lbl = toolkit.createLabel(parent, label);
+       // lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
+       // lbl.setFont(EclipseUiUtils.getBoldFont(parent));
+       // Text text = toolkit.createText(parent, value, SWT.NONE);
+       // text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+       // text.setEditable(false);
+       // CmsUiUtils.style(text, CmsWorkbenchStyles.WORKBENCH_FORM_TEXT);
+       // return text;
+       // }
+
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/GroupsView.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/GroupsView.java
new file mode 100644 (file)
index 0000000..d941159
--- /dev/null
@@ -0,0 +1,251 @@
+package org.argeo.cms.e4.users;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+
+import org.argeo.api.acr.ldap.LdapAttr;
+import org.argeo.api.acr.ldap.LdapObj;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.CurrentUser;
+import org.argeo.cms.e4.users.providers.CommonNameLP;
+import org.argeo.cms.e4.users.providers.DomainNameLP;
+import org.argeo.cms.e4.users.providers.RoleIconLP;
+import org.argeo.cms.e4.users.providers.UserDragListener;
+import org.argeo.cms.swt.CmsException;
+import org.argeo.cms.swt.useradmin.LdifUsersTable;
+//import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+//import org.argeo.cms.ui.workbench.internal.useradmin.UiUserAdminListener;
+//import org.argeo.cms.ui.workbench.internal.useradmin.UserAdminWrapper;
+//import org.argeo.cms.ui.workbench.internal.useradmin.providers.CommonNameLP;
+//import org.argeo.cms.ui.workbench.internal.useradmin.providers.DomainNameLP;
+//import org.argeo.cms.ui.workbench.internal.useradmin.providers.RoleIconLP;
+//import org.argeo.cms.ui.workbench.internal.useradmin.providers.UserDragListener;
+//import org.argeo.cms.ui.workbench.internal.useradmin.providers.UserTableDefaultDClickListener;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.e4.ui.di.Focus;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+//import org.eclipse.ui.part.ViewPart;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/** List all groups with filter */
+public class GroupsView {
+       private final static CmsLog log = CmsLog.getLog(GroupsView.class);
+       // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".groupsView";
+
+       @Inject
+       private EPartService partService;
+       @Inject
+       private UserAdminWrapper userAdminWrapper;
+
+       // UI Objects
+       private LdifUsersTable groupTableViewerCmp;
+       private TableViewer userViewer;
+       private List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+
+       private UserAdminListener listener;
+
+       @PostConstruct
+       public void createPartControl(Composite parent, ESelectionService selectionService) {
+               parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+               // boolean isAdmin = CurrentUser.isInRole(NodeConstants.ROLE_ADMIN);
+
+               // Define the displayed columns
+               columnDefs.add(new ColumnDefinition(new RoleIconLP(), "", 19));
+               columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Name", 150));
+               columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 100));
+               // Only show technical DN to admin
+               // if (isAdmin)
+               // columnDefs.add(new ColumnDefinition(new UserNameLP(),
+               // "Distinguished Name", 300));
+
+               // Create and configure the table
+               groupTableViewerCmp = new MyUserTableViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+
+               groupTableViewerCmp.setColumnDefinitions(columnDefs);
+               // if (isAdmin)
+               // groupTableViewerCmp.populateWithStaticFilters(false, false);
+               // else
+               groupTableViewerCmp.populate(true, false);
+
+               groupTableViewerCmp.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               // Links
+               userViewer = groupTableViewerCmp.getTableViewer();
+               userViewer.addDoubleClickListener(new UserTableDefaultDClickListener(partService));
+               // getViewSite().setSelectionProvider(userViewer);
+               userViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+
+                       @Override
+                       public void selectionChanged(SelectionChangedEvent event) {
+                               IStructuredSelection selection = (IStructuredSelection) event.getSelection();
+                               selectionService.setSelection(selection.toList());
+                       }
+               });
+
+               // Really?
+               groupTableViewerCmp.refresh();
+
+               // Drag and drop
+               int operations = DND.DROP_COPY | DND.DROP_MOVE;
+               Transfer[] tt = new Transfer[] { TextTransfer.getInstance() };
+               userViewer.addDragSupport(operations, tt, new UserDragListener(userViewer));
+
+               // // Register a useradmin listener
+               // listener = new UserAdminListener() {
+               // @Override
+               // public void roleChanged(UserAdminEvent event) {
+               // if (userViewer != null && !userViewer.getTable().isDisposed())
+               // refresh();
+               // }
+               // };
+               // userAdminWrapper.addListener(listener);
+               // }
+
+               // Register a useradmin listener
+               listener = new MyUiUAListener(parent.getDisplay());
+               userAdminWrapper.addListener(listener);
+       }
+
+       private class MyUiUAListener extends UiUserAdminListener {
+               public MyUiUAListener(Display display) {
+                       super(display);
+               }
+
+               @Override
+               public void roleChangedToUiThread(UserAdminEvent event) {
+                       if (userViewer != null && !userViewer.getTable().isDisposed())
+                               refresh();
+               }
+       }
+
+       private class MyUserTableViewer extends LdifUsersTable {
+               private static final long serialVersionUID = 8467999509931900367L;
+
+               private boolean showSystemRoles = true;
+
+               private final String[] knownProps = { LdapAttr.uid.name(), LdapAttr.cn.name(), LdapAttr.DN };
+
+               public MyUserTableViewer(Composite parent, int style) {
+                       super(parent, style);
+                       showSystemRoles = CurrentUser.isInRole(CmsConstants.ROLE_ADMIN);
+               }
+
+               protected void populateStaticFilters(Composite staticFilterCmp) {
+                       staticFilterCmp.setLayout(new GridLayout());
+                       final Button showSystemRoleBtn = new Button(staticFilterCmp, SWT.CHECK);
+                       showSystemRoleBtn.setText("Show system roles");
+                       showSystemRoles = CurrentUser.isInRole(CmsConstants.ROLE_ADMIN);
+                       showSystemRoleBtn.setSelection(showSystemRoles);
+
+                       showSystemRoleBtn.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = -7033424592697691676L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       showSystemRoles = showSystemRoleBtn.getSelection();
+                                       refresh();
+                               }
+
+                       });
+               }
+
+               @Override
+               protected List<User> listFilteredElements(String filter) {
+                       Role[] roles;
+                       try {
+                               StringBuilder builder = new StringBuilder();
+                               StringBuilder tmpBuilder = new StringBuilder();
+                               if (EclipseUiUtils.notEmpty(filter))
+                                       for (String prop : knownProps) {
+                                               tmpBuilder.append("(");
+                                               tmpBuilder.append(prop);
+                                               tmpBuilder.append("=*");
+                                               tmpBuilder.append(filter);
+                                               tmpBuilder.append("*)");
+                                       }
+                               if (tmpBuilder.length() > 1) {
+                                       builder.append("(&(").append(LdapAttr.objectClass.name()).append("=")
+                                                       .append(LdapObj.groupOfNames.name()).append(")");
+                                       // hide tokens
+                                       builder.append("(!(").append(LdapAttr.DN).append("=*").append(CmsConstants.TOKENS_BASEDN)
+                                                       .append("))");
+
+                                       if (!showSystemRoles)
+                                               builder.append("(!(").append(LdapAttr.DN).append("=*").append(CmsConstants.SYSTEM_ROLES_BASEDN)
+                                                               .append("))");
+                                       builder.append("(|");
+                                       builder.append(tmpBuilder.toString());
+                                       builder.append("))");
+                               } else {
+                                       if (!showSystemRoles)
+                                               builder.append("(&(").append(LdapAttr.objectClass.name()).append("=")
+                                                               .append(LdapObj.groupOfNames.name()).append(")(!(").append(LdapAttr.DN).append("=*")
+                                                               .append(CmsConstants.SYSTEM_ROLES_BASEDN).append("))(!(").append(LdapAttr.DN).append("=*")
+                                                               .append(CmsConstants.TOKENS_BASEDN).append(")))");
+                                       else
+                                               builder.append("(&(").append(LdapAttr.objectClass.name()).append("=")
+                                                               .append(LdapObj.groupOfNames.name()).append(")(!(").append(LdapAttr.DN).append("=*")
+                                                               .append(CmsConstants.TOKENS_BASEDN).append(")))");
+
+                               }
+                               roles = userAdminWrapper.getUserAdmin().getRoles(builder.toString());
+                       } catch (InvalidSyntaxException e) {
+                               throw new CmsException("Unable to get roles with filter: " + filter, e);
+                       }
+                       List<User> users = new ArrayList<User>();
+                       for (Role role : roles)
+                               if (!users.contains(role))
+                                       users.add((User) role);
+                               else
+                                       log.warn("Duplicated role: " + role);
+
+                       return users;
+               }
+       }
+
+       public void refresh() {
+               groupTableViewerCmp.refresh();
+       }
+
+       @PreDestroy
+       public void dispose() {
+               userAdminWrapper.removeListener(listener);
+       }
+
+       @Focus
+       public void setFocus() {
+               groupTableViewerCmp.setFocus();
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/SecurityAdminImages.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/SecurityAdminImages.java
new file mode 100644 (file)
index 0000000..7bbe3c7
--- /dev/null
@@ -0,0 +1,19 @@
+package org.argeo.cms.e4.users;
+
+import org.argeo.cms.ui.theme.CmsImages;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.Image;
+
+/** Shared icons that must be declared programmatically . */
+public class SecurityAdminImages extends CmsImages {
+       private final static String PREFIX = "icons/";
+
+       public final static ImageDescriptor ICON_REMOVE_DESC = createDesc(PREFIX + "delete.png");
+       public final static ImageDescriptor ICON_USER_DESC = createDesc(PREFIX + "person.png");
+
+       public final static Image ICON_USER = ICON_USER_DESC.createImage();
+       public final static Image ICON_GROUP = createImg(PREFIX + "group.png");
+       public final static Image ICON_WORKGROUP = createImg(PREFIX + "workgroup.png");
+       public final static Image ICON_ROLE = createImg(PREFIX + "role.gif");
+
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UiAdminUtils.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UiAdminUtils.java
new file mode 100644 (file)
index 0000000..bc9fd83
--- /dev/null
@@ -0,0 +1,34 @@
+package org.argeo.cms.e4.users;
+
+import org.argeo.api.cms.transaction.WorkTransaction;
+
+/** First effort to centralize back end methods used by the user admin UI */
+public class UiAdminUtils {
+       /*
+        * INTERNAL METHODS: Below methods are meant to stay here and are not part
+        * of a potential generic backend to manage the useradmin
+        */
+       /** Easily notify the ActiveWindow that the transaction had a state change */
+       public final static void notifyTransactionStateChange(
+                       WorkTransaction userTransaction) {
+//             try {
+//                     IWorkbenchWindow aww = PlatformUI.getWorkbench()
+//                                     .getActiveWorkbenchWindow();
+//                     ISourceProviderService sourceProviderService = (ISourceProviderService) aww
+//                                     .getService(ISourceProviderService.class);
+//                     UserTransactionProvider esp = (UserTransactionProvider) sourceProviderService
+//                                     .getSourceProvider(UserTransactionProvider.TRANSACTION_STATE);
+//                     esp.fireTransactionStateChange();
+//             } catch (Exception e) {
+//                     throw new CmsException("Unable to begin transaction", e);
+//             }
+       }
+
+       /**
+        * Email addresses must match this regexp pattern ({@value #EMAIL_PATTERN}.
+        * Thanks to <a href=
+        * "http://www.mkyong.com/regular-expressions/how-to-validate-email-address-with-regular-expression/"
+        * >this tip</a>.
+        */
+       public final static String EMAIL_PATTERN = "^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UiUserAdminListener.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UiUserAdminListener.java
new file mode 100644 (file)
index 0000000..eb64aba
--- /dev/null
@@ -0,0 +1,27 @@
+package org.argeo.cms.e4.users;
+
+import org.eclipse.swt.widgets.Display;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/** Convenience class to insure the call to refresh is done in the UI thread */
+public abstract class UiUserAdminListener implements UserAdminListener {
+
+       private final Display display;
+
+       public UiUserAdminListener(Display display) {
+               this.display = display;
+       }
+
+       @Override
+       public void roleChanged(final UserAdminEvent event) {
+               display.asyncExec(new Runnable() {
+                       @Override
+                       public void run() {
+                               roleChangedToUiThread(event);
+                       }
+               });
+       }
+
+       public abstract void roleChangedToUiThread(UserAdminEvent event);
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserAdminWrapper.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserAdminWrapper.java
new file mode 100644 (file)
index 0000000..9f333a7
--- /dev/null
@@ -0,0 +1,153 @@
+package org.argeo.cms.e4.users;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.directory.UserDirectory;
+import org.argeo.api.cms.transaction.WorkTransaction;
+import org.argeo.cms.runtime.DirectoryConf;
+import org.argeo.cms.swt.CmsException;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/** Centralise interaction with the UserAdmin in this bundle */
+public class UserAdminWrapper {
+
+       private UserAdmin userAdmin;
+       // private ServiceReference<UserAdmin> userAdminServiceReference;
+//     private Set<String> uris;
+       private Map<UserDirectory, Hashtable<String, String>> userDirectories = Collections
+                       .synchronizedMap(new LinkedHashMap<>());
+       private WorkTransaction userTransaction;
+
+       // First effort to simplify UX while managing users and groups
+       public final static boolean COMMIT_ON_SAVE = true;
+
+       // Registered listeners
+       List<UserAdminListener> listeners = new ArrayList<UserAdminListener>();
+
+       /**
+        * Starts a transaction if necessary. Should always been called together with
+        * {@link UserAdminWrapper#commitOrNotifyTransactionStateChange()} once the
+        * security model changes have been performed.
+        */
+       public WorkTransaction beginTransactionIfNeeded() {
+               try {
+                       // UserTransaction userTransaction = getUserTransaction();
+                       if (userTransaction.isNoTransactionStatus()) {
+                               userTransaction.begin();
+                               // UiAdminUtils.notifyTransactionStateChange(userTransaction);
+                       }
+                       return userTransaction;
+               } catch (Exception e) {
+                       throw new CmsException("Unable to begin transaction", e);
+               }
+       }
+
+       /**
+        * Depending on the current application configuration, it will either commit the
+        * current transaction or throw a notification that the transaction state has
+        * changed (In the later case, it must be called from the UI thread).
+        */
+       public void commitOrNotifyTransactionStateChange() {
+               try {
+                       // UserTransaction userTransaction = getUserTransaction();
+                       if (userTransaction.isNoTransactionStatus())
+                               return;
+
+                       if (UserAdminWrapper.COMMIT_ON_SAVE)
+                               userTransaction.commit();
+                       else
+                               UiAdminUtils.notifyTransactionStateChange(userTransaction);
+               } catch (Exception e) {
+                       throw new CmsException("Unable to clean transaction", e);
+               }
+       }
+
+       // TODO implement safer mechanism
+       public void addListener(UserAdminListener userAdminListener) {
+               if (!listeners.contains(userAdminListener))
+                       listeners.add(userAdminListener);
+       }
+
+       public void removeListener(UserAdminListener userAdminListener) {
+               if (listeners.contains(userAdminListener))
+                       listeners.remove(userAdminListener);
+       }
+
+       public void notifyListeners(UserAdminEvent event) {
+               for (UserAdminListener listener : listeners)
+                       listener.roleChanged(event);
+       }
+
+       public Map<String, String> getKnownBaseDns(boolean onlyWritable) {
+               Map<String, String> dns = new HashMap<String, String>();
+               for (UserDirectory userDirectory : userDirectories.keySet()) {
+                       Boolean readOnly = userDirectory.isReadOnly();
+                       String baseDn = userDirectory.getBase();
+
+                       if (onlyWritable && readOnly)
+                               continue;
+                       if (baseDn.equalsIgnoreCase(CmsConstants.SYSTEM_ROLES_BASEDN))
+                               continue;
+                       if (baseDn.equalsIgnoreCase(CmsConstants.TOKENS_BASEDN))
+                               continue;
+                       dns.put(baseDn, DirectoryConf.propertiesAsUri(userDirectories.get(userDirectory)).toString());
+
+               }
+//             for (String uri : uris) {
+//                     if (!uri.startsWith("/"))
+//                             continue;
+//                     Dictionary<String, ?> props = UserAdminConf.uriAsProperties(uri);
+//                     String readOnly = UserAdminConf.readOnly.getValue(props);
+//                     String baseDn = UserAdminConf.baseDn.getValue(props);
+//
+//                     if (onlyWritable && "true".equals(readOnly))
+//                             continue;
+//                     if (baseDn.equalsIgnoreCase(NodeConstants.ROLES_BASEDN))
+//                             continue;
+//                     if (baseDn.equalsIgnoreCase(NodeConstants.TOKENS_BASEDN))
+//                             continue;
+//                     dns.put(baseDn, uri);
+//             }
+               return dns;
+       }
+
+       public UserAdmin getUserAdmin() {
+               return userAdmin;
+       }
+
+       public WorkTransaction getUserTransaction() {
+               return userTransaction;
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdmin(UserAdmin userAdmin, Map<String, String> properties) {
+               this.userAdmin = userAdmin;
+//             this.uris = Collections.unmodifiableSortedSet(new TreeSet<>(properties.keySet()));
+       }
+
+       public void setUserTransaction(WorkTransaction userTransaction) {
+               this.userTransaction = userTransaction;
+       }
+
+       public void addUserDirectory(UserDirectory userDirectory, Map<String, String> properties) {
+               userDirectories.put(userDirectory, new Hashtable<>(properties));
+       }
+
+       public void removeUserDirectory(UserDirectory userDirectory, Map<String, String> properties) {
+               userDirectories.remove(userDirectory);
+       }
+
+       // public void setUserAdminServiceReference(
+       // ServiceReference<UserAdmin> userAdminServiceReference) {
+       // this.userAdminServiceReference = userAdminServiceReference;
+       // }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserBatchUpdateWizard.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserBatchUpdateWizard.java
new file mode 100644 (file)
index 0000000..07efcbb
--- /dev/null
@@ -0,0 +1,606 @@
+package org.argeo.cms.e4.users;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.argeo.api.acr.ldap.LdapAttr;
+import org.argeo.api.acr.ldap.LdapObj;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.api.cms.transaction.WorkTransaction;
+import org.argeo.cms.CurrentUser;
+import org.argeo.cms.auth.UserAdminUtils;
+import org.argeo.cms.e4.users.providers.CommonNameLP;
+import org.argeo.cms.e4.users.providers.DomainNameLP;
+import org.argeo.cms.e4.users.providers.MailLP;
+import org.argeo.cms.e4.users.providers.UserNameLP;
+import org.argeo.cms.swt.CmsException;
+import org.argeo.cms.swt.useradmin.LdifUsersTable;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.jface.dialogs.IPageChangeProvider;
+import org.eclipse.jface.dialogs.IPageChangedListener;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.PageChangedEvent;
+import org.eclipse.jface.wizard.IWizardContainer;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Text;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Wizard to update users */
+public class UserBatchUpdateWizard extends Wizard {
+
+       private final static CmsLog log = CmsLog.getLog(UserBatchUpdateWizard.class);
+       private UserAdminWrapper userAdminWrapper;
+
+       // pages
+       private ChooseCommandWizardPage chooseCommandPage;
+       private ChooseUsersWizardPage userListPage;
+       private ValidateAndLaunchWizardPage validatePage;
+
+       // Various implemented commands keys
+       private final static String CMD_UPDATE_PASSWORD = "resetPassword";
+       private final static String CMD_UPDATE_EMAIL = "resetEmail";
+       private final static String CMD_GROUP_MEMBERSHIP = "groupMembership";
+
+       private final Map<String, String> commands = new HashMap<String, String>() {
+               private static final long serialVersionUID = 1L;
+               {
+                       put("Reset password(s)", CMD_UPDATE_PASSWORD);
+                       put("Reset email(s)", CMD_UPDATE_EMAIL);
+                       // TODO implement role / group management
+                       // put("Add/Remove from group", CMD_GROUP_MEMBERSHIP);
+               }
+       };
+
+       public UserBatchUpdateWizard(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+
+       @Override
+       public void addPages() {
+               chooseCommandPage = new ChooseCommandWizardPage();
+               addPage(chooseCommandPage);
+               userListPage = new ChooseUsersWizardPage();
+               addPage(userListPage);
+               validatePage = new ValidateAndLaunchWizardPage();
+               addPage(validatePage);
+       }
+
+       @Override
+       public boolean performFinish() {
+               if (!canFinish())
+                       return false;
+               WorkTransaction ut = userAdminWrapper.getUserTransaction();
+               if (!ut.isNoTransactionStatus() && !MessageDialog.openConfirm(getShell(), "Existing Transaction",
+                               "A user transaction is already existing, " + "are you sure you want to proceed ?"))
+                       return false;
+
+               // We cannot use jobs, user modifications are still meant to be done in
+               // the UIThread
+               // UpdateJob job = null;
+               // if (job != null)
+               // job.schedule();
+
+               if (CMD_UPDATE_PASSWORD.equals(chooseCommandPage.getCommand())) {
+                       char[] newValue = chooseCommandPage.getPwdValue();
+                       if (newValue == null)
+                               throw new CmsException("Password cannot be null or an empty string");
+                       ResetPassword job = new ResetPassword(userAdminWrapper, userListPage.getSelectedUsers(), newValue);
+                       job.doUpdate();
+               } else if (CMD_UPDATE_EMAIL.equals(chooseCommandPage.getCommand())) {
+                       String newValue = chooseCommandPage.getEmailValue();
+                       if (newValue == null)
+                               throw new CmsException("Password cannot be null or an empty string");
+                       ResetEmail job = new ResetEmail(userAdminWrapper, userListPage.getSelectedUsers(), newValue);
+                       job.doUpdate();
+               }
+               return true;
+       }
+
+       public boolean canFinish() {
+               if (this.getContainer().getCurrentPage() == validatePage)
+                       return true;
+               return false;
+       }
+
+       private class ResetPassword {
+               private char[] newPwd;
+               private UserAdminWrapper userAdminWrapper;
+               private List<User> usersToUpdate;
+
+               public ResetPassword(UserAdminWrapper userAdminWrapper, List<User> usersToUpdate, char[] newPwd) {
+                       this.newPwd = newPwd;
+                       this.usersToUpdate = usersToUpdate;
+                       this.userAdminWrapper = userAdminWrapper;
+               }
+
+               @SuppressWarnings("unchecked")
+               protected void doUpdate() {
+                       userAdminWrapper.beginTransactionIfNeeded();
+                       try {
+                               for (User user : usersToUpdate) {
+                                       // the char array is emptied after being used.
+                                       user.getCredentials().put(null, newPwd.clone());
+                               }
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                       } catch (Exception e) {
+                               throw new CmsException("Cannot perform batch update on users", e);
+                       } finally {
+                               WorkTransaction ut = userAdminWrapper.getUserTransaction();
+                               if (!ut.isNoTransactionStatus())
+                                       ut.rollback();
+                       }
+               }
+       }
+
+       private class ResetEmail {
+               private String newEmail;
+               private UserAdminWrapper userAdminWrapper;
+               private List<User> usersToUpdate;
+
+               public ResetEmail(UserAdminWrapper userAdminWrapper, List<User> usersToUpdate, String newEmail) {
+                       this.newEmail = newEmail;
+                       this.usersToUpdate = usersToUpdate;
+                       this.userAdminWrapper = userAdminWrapper;
+               }
+
+               @SuppressWarnings("unchecked")
+               protected void doUpdate() {
+                       userAdminWrapper.beginTransactionIfNeeded();
+                       try {
+                               for (User user : usersToUpdate) {
+                                       // the char array is emptied after being used.
+                                       user.getProperties().put(LdapAttr.mail.name(), newEmail);
+                               }
+
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               if (!usersToUpdate.isEmpty())
+                                       userAdminWrapper.notifyListeners(
+                                                       new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, usersToUpdate.get(0)));
+                       } catch (Exception e) {
+                               throw new CmsException("Cannot perform batch update on users", e);
+                       } finally {
+                               WorkTransaction ut = userAdminWrapper.getUserTransaction();
+                               if (!ut.isNoTransactionStatus())
+                                       ut.rollback();
+                       }
+               }
+       }
+
+       // @SuppressWarnings("unused")
+       // private class AddToGroup extends UpdateJob {
+       // private String groupID;
+       // private Session session;
+       //
+       // public AddToGroup(Session session, List<Node> nodesToUpdate,
+       // String groupID) {
+       // super(session, nodesToUpdate);
+       // this.session = session;
+       // this.groupID = groupID;
+       // }
+       //
+       // protected void doUpdate(Node node) {
+       // log.info("Add/Remove to group actions are not yet implemented");
+       // // TODO implement this
+       // // try {
+       // // throw new CmsException("Not yet implemented");
+       // // } catch (RepositoryException re) {
+       // // throw new CmsException(
+       // // "Unable to update boolean value for node " + node, re);
+       // // }
+       // }
+       // }
+
+       // /**
+       // * Base privileged job that will be run asynchronously to perform the
+       // batch
+       // * update
+       // */
+       // private abstract class UpdateJob extends PrivilegedJob {
+       //
+       // private final UserAdminWrapper userAdminWrapper;
+       // private final List<User> usersToUpdate;
+       //
+       // protected abstract void doUpdate(User user);
+       //
+       // public UpdateJob(UserAdminWrapper userAdminWrapper,
+       // List<User> usersToUpdate) {
+       // super("Perform update");
+       // this.usersToUpdate = usersToUpdate;
+       // this.userAdminWrapper = userAdminWrapper;
+       // }
+       //
+       // @Override
+       // protected IStatus doRun(IProgressMonitor progressMonitor) {
+       // try {
+       // JcrMonitor monitor = new EclipseJcrMonitor(progressMonitor);
+       // int total = usersToUpdate.size();
+       // monitor.beginTask("Performing change", total);
+       // userAdminWrapper.beginTransactionIfNeeded();
+       // for (User user : usersToUpdate) {
+       // doUpdate(user);
+       // monitor.worked(1);
+       // }
+       // userAdminWrapper.getUserTransaction().commit();
+       // } catch (Exception e) {
+       // throw new CmsException(
+       // "Cannot perform batch update on users", e);
+       // } finally {
+       // UserTransaction ut = userAdminWrapper.getUserTransaction();
+       // try {
+       // if (ut.getStatus() != javax.transaction.Status.STATUS_NO_TRANSACTION)
+       // ut.rollback();
+       // } catch (IllegalStateException | SecurityException
+       // | SystemException e) {
+       // log.error("Unable to rollback session in 'finally', "
+       // + "the system might be in a dirty state");
+       // e.printStackTrace();
+       // }
+       // }
+       // return Status.OK_STATUS;
+       // }
+       // }
+
+       // PAGES
+       /**
+        * Displays a combo box that enables user to choose which action to perform
+        */
+       private class ChooseCommandWizardPage extends WizardPage {
+               private static final long serialVersionUID = -8069434295293996633L;
+               private Combo chooseCommandCmb;
+               private Button trueChk;
+               private Text valueTxt;
+               private Text pwdTxt;
+               private Text pwd2Txt;
+
+               public ChooseCommandWizardPage() {
+                       super("Choose a command to run.");
+                       setTitle("Choose a command to run.");
+               }
+
+               @Override
+               public void createControl(Composite parent) {
+                       GridLayout gl = new GridLayout();
+                       Composite container = new Composite(parent, SWT.NO_FOCUS);
+                       container.setLayout(gl);
+
+                       chooseCommandCmb = new Combo(container, SWT.READ_ONLY);
+                       chooseCommandCmb.setLayoutData(EclipseUiUtils.fillWidth());
+                       String[] values = commands.keySet().toArray(new String[0]);
+                       chooseCommandCmb.setItems(values);
+
+                       final Composite bottomPart = new Composite(container, SWT.NO_FOCUS);
+                       bottomPart.setLayoutData(EclipseUiUtils.fillAll());
+                       bottomPart.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+                       chooseCommandCmb.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = 1L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       if (getCommand().equals(CMD_UPDATE_PASSWORD))
+                                               populatePasswordCmp(bottomPart);
+                                       else if (getCommand().equals(CMD_UPDATE_EMAIL))
+                                               populateEmailCmp(bottomPart);
+                                       else if (getCommand().equals(CMD_GROUP_MEMBERSHIP))
+                                               populateGroupCmp(bottomPart);
+                                       else
+                                               populateBooleanFlagCmp(bottomPart);
+                                       checkPageComplete();
+                                       bottomPart.layout(true, true);
+                               }
+                       });
+                       setControl(container);
+               }
+
+               private void populateBooleanFlagCmp(Composite parent) {
+                       EclipseUiUtils.clear(parent);
+                       trueChk = new Button(parent, SWT.CHECK);
+                       trueChk.setText("Do it. (It will to the contrary if unchecked)");
+                       trueChk.setSelection(true);
+                       trueChk.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false));
+               }
+
+               private void populatePasswordCmp(Composite parent) {
+                       EclipseUiUtils.clear(parent);
+                       Composite body = new Composite(parent, SWT.NO_FOCUS);
+
+                       ModifyListener ml = new ModifyListener() {
+                               private static final long serialVersionUID = -1558726363536729634L;
+
+                               @Override
+                               public void modifyText(ModifyEvent event) {
+                                       checkPageComplete();
+                               }
+                       };
+
+                       body.setLayout(new GridLayout(2, false));
+                       body.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       pwdTxt = EclipseUiUtils.createGridLP(body, "New password", ml);
+                       pwd2Txt = EclipseUiUtils.createGridLP(body, "Repeat password", ml);
+               }
+
+               private void populateEmailCmp(Composite parent) {
+                       EclipseUiUtils.clear(parent);
+                       Composite body = new Composite(parent, SWT.NO_FOCUS);
+
+                       ModifyListener ml = new ModifyListener() {
+                               private static final long serialVersionUID = 2147704227294268317L;
+
+                               @Override
+                               public void modifyText(ModifyEvent event) {
+                                       checkPageComplete();
+                               }
+                       };
+
+                       body.setLayout(new GridLayout(2, false));
+                       body.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       valueTxt = EclipseUiUtils.createGridLT(body, "New e-mail", ml);
+               }
+
+               private void checkPageComplete() {
+                       String errorMsg = null;
+                       if (chooseCommandCmb.getSelectionIndex() < 0)
+                               errorMsg = "Please select an action";
+                       else if (CMD_UPDATE_EMAIL.equals(getCommand())) {
+                               if (!valueTxt.getText().matches(UiAdminUtils.EMAIL_PATTERN))
+                                       errorMsg = "Not a valid e-mail address";
+                       } else if (CMD_UPDATE_PASSWORD.equals(getCommand())) {
+                               if (EclipseUiUtils.isEmpty(pwdTxt.getText()) || pwdTxt.getText().length() < 4)
+                                       errorMsg = "Please enter a password that is at least 4 character long";
+                               else if (!pwdTxt.getText().equals(pwd2Txt.getText()))
+                                       errorMsg = "Passwords are different";
+                       }
+                       if (EclipseUiUtils.notEmpty(errorMsg)) {
+                               setMessage(errorMsg, WizardPage.ERROR);
+                               setPageComplete(false);
+                       } else {
+                               setMessage("Page complete, you can proceed to user choice", WizardPage.INFORMATION);
+                               setPageComplete(true);
+                       }
+
+                       getContainer().updateButtons();
+               }
+
+               private void populateGroupCmp(Composite parent) {
+                       EclipseUiUtils.clear(parent);
+                       trueChk = new Button(parent, SWT.CHECK);
+                       trueChk.setText("Add to group. (It will remove user(s) from the " + "corresponding group if unchecked)");
+                       trueChk.setSelection(true);
+                       trueChk.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false));
+               }
+
+               protected String getCommand() {
+                       return commands.get(chooseCommandCmb.getItem(chooseCommandCmb.getSelectionIndex()));
+               }
+
+               protected String getCommandLbl() {
+                       return chooseCommandCmb.getItem(chooseCommandCmb.getSelectionIndex());
+               }
+
+               @SuppressWarnings("unused")
+               protected boolean getBoleanValue() {
+                       // FIXME this is not consistent and will lead to errors.
+                       if ("argeo:enabled".equals(getCommand()))
+                               return trueChk.getSelection();
+                       else
+                               return !trueChk.getSelection();
+               }
+
+               @SuppressWarnings("unused")
+               protected String getStringValue() {
+                       String value = null;
+                       if (valueTxt != null) {
+                               value = valueTxt.getText();
+                               if ("".equals(value.trim()))
+                                       value = null;
+                       }
+                       return value;
+               }
+
+               protected char[] getPwdValue() {
+                       // We do not directly reset the password text fields: There is no
+                       // need to over secure this process: setting a pwd to multi users
+                       // at the same time is anyhow a bad practice and should be used only
+                       // in test environment or for temporary access
+                       if (pwdTxt == null || pwdTxt.isDisposed())
+                               return null;
+                       else
+                               return pwdTxt.getText().toCharArray();
+               }
+
+               protected String getEmailValue() {
+                       // We do not directly reset the password text fields: There is no
+                       // need to over secure this process: setting a pwd to multi users
+                       // at the same time is anyhow a bad practice and should be used only
+                       // in test environment or for temporary access
+                       if (valueTxt == null || valueTxt.isDisposed())
+                               return null;
+                       else
+                               return valueTxt.getText();
+               }
+       }
+
+       /**
+        * Displays a list of users with a check box to be able to choose some of them
+        */
+       private class ChooseUsersWizardPage extends WizardPage implements IPageChangedListener {
+               private static final long serialVersionUID = 7651807402211214274L;
+               private ChooseUserTableViewer userTableCmp;
+
+               public ChooseUsersWizardPage() {
+                       super("Choose Users");
+                       setTitle("Select users who will be impacted");
+               }
+
+               @Override
+               public void createControl(Composite parent) {
+                       Composite pageCmp = new Composite(parent, SWT.NONE);
+                       pageCmp.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+                       // Define the displayed columns
+                       List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+                       columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Common Name", 150));
+                       columnDefs.add(new ColumnDefinition(new MailLP(), "E-mail", 150));
+                       columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 200));
+
+                       // Only show technical DN to admin
+                       if (CurrentUser.isInRole(CmsConstants.ROLE_ADMIN))
+                               columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name", 300));
+
+                       userTableCmp = new ChooseUserTableViewer(pageCmp, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+                       userTableCmp.setLayoutData(EclipseUiUtils.fillAll());
+                       userTableCmp.setColumnDefinitions(columnDefs);
+                       userTableCmp.populate(true, true);
+                       userTableCmp.refresh();
+
+                       setControl(pageCmp);
+
+                       // Add listener to update message when shown
+                       final IWizardContainer wContainer = this.getContainer();
+                       if (wContainer instanceof IPageChangeProvider) {
+                               ((IPageChangeProvider) wContainer).addPageChangedListener(this);
+                       }
+
+               }
+
+               @Override
+               public void pageChanged(PageChangedEvent event) {
+                       if (event.getSelectedPage() == this) {
+                               String msg = "Chosen batch action: " + chooseCommandPage.getCommandLbl();
+                               ((WizardPage) event.getSelectedPage()).setMessage(msg);
+                       }
+               }
+
+               protected List<User> getSelectedUsers() {
+                       return userTableCmp.getSelectedUsers();
+               }
+
+               private class ChooseUserTableViewer extends LdifUsersTable {
+                       private static final long serialVersionUID = 5080437561015853124L;
+                       private final String[] knownProps = { LdapAttr.uid.name(), LdapAttr.DN, LdapAttr.cn.name(),
+                                       LdapAttr.givenName.name(), LdapAttr.sn.name(), LdapAttr.mail.name() };
+
+                       public ChooseUserTableViewer(Composite parent, int style) {
+                               super(parent, style);
+                       }
+
+                       @Override
+                       protected List<User> listFilteredElements(String filter) {
+                               Role[] roles;
+
+                               try {
+                                       StringBuilder builder = new StringBuilder();
+
+                                       StringBuilder tmpBuilder = new StringBuilder();
+                                       if (EclipseUiUtils.notEmpty(filter))
+                                               for (String prop : knownProps) {
+                                                       tmpBuilder.append("(");
+                                                       tmpBuilder.append(prop);
+                                                       tmpBuilder.append("=*");
+                                                       tmpBuilder.append(filter);
+                                                       tmpBuilder.append("*)");
+                                               }
+                                       if (tmpBuilder.length() > 1) {
+                                               builder.append("(&(").append(LdapAttr.objectClass.name()).append("=")
+                                                               .append(LdapObj.inetOrgPerson.name()).append(")(|");
+                                               builder.append(tmpBuilder.toString());
+                                               builder.append("))");
+                                       } else
+                                               builder.append("(").append(LdapAttr.objectClass.name()).append("=")
+                                                               .append(LdapObj.inetOrgPerson.name()).append(")");
+                                       roles = userAdminWrapper.getUserAdmin().getRoles(builder.toString());
+                               } catch (InvalidSyntaxException e) {
+                                       throw new CmsException("Unable to get roles with filter: " + filter, e);
+                               }
+                               List<User> users = new ArrayList<User>();
+                               for (Role role : roles)
+                                       // Prevent current logged in user to perform batch on
+                                       // himself
+                                       if (!UserAdminUtils.isCurrentUser((User) role))
+                                               users.add((User) role);
+                               return users;
+                       }
+               }
+       }
+
+       /** Summary of input data before launching the process */
+       private class ValidateAndLaunchWizardPage extends WizardPage implements IPageChangedListener {
+               private static final long serialVersionUID = 7098918351451743853L;
+               private ChosenUsersTableViewer userTableCmp;
+
+               public ValidateAndLaunchWizardPage() {
+                       super("Validate and launch");
+                       setTitle("Validate and launch");
+               }
+
+               @Override
+               public void createControl(Composite parent) {
+                       Composite pageCmp = new Composite(parent, SWT.NO_FOCUS);
+                       pageCmp.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+                       List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+                       columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Common Name", 150));
+                       columnDefs.add(new ColumnDefinition(new MailLP(), "E-mail", 150));
+                       columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 200));
+                       // Only show technical DN to admin
+                       if (CurrentUser.isInRole(CmsConstants.ROLE_ADMIN))
+                               columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name", 300));
+                       userTableCmp = new ChosenUsersTableViewer(pageCmp, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+                       userTableCmp.setLayoutData(EclipseUiUtils.fillAll());
+                       userTableCmp.setColumnDefinitions(columnDefs);
+                       userTableCmp.populate(false, false);
+                       userTableCmp.refresh();
+                       setControl(pageCmp);
+                       // Add listener to update message when shown
+                       final IWizardContainer wContainer = this.getContainer();
+                       if (wContainer instanceof IPageChangeProvider) {
+                               ((IPageChangeProvider) wContainer).addPageChangedListener(this);
+                       }
+               }
+
+               @Override
+               public void pageChanged(PageChangedEvent event) {
+                       if (event.getSelectedPage() == this) {
+                               @SuppressWarnings({ "unchecked", "rawtypes" })
+                               Object[] values = ((ArrayList) userListPage.getSelectedUsers())
+                                               .toArray(new Object[userListPage.getSelectedUsers().size()]);
+                               userTableCmp.getTableViewer().setInput(values);
+                               String msg = "Following batch action: [" + chooseCommandPage.getCommandLbl()
+                                               + "] will be perfomed on the users listed below.\n";
+                               // + "Are you sure you want to proceed?";
+                               setMessage(msg);
+                       }
+               }
+
+               private class ChosenUsersTableViewer extends LdifUsersTable {
+                       private static final long serialVersionUID = 7814764735794270541L;
+
+                       public ChosenUsersTableViewer(Composite parent, int style) {
+                               super(parent, style);
+                       }
+
+                       @Override
+                       protected List<User> listFilteredElements(String filter) {
+                               return userListPage.getSelectedUsers();
+                       }
+               }
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserEditor.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserEditor.java
new file mode 100644 (file)
index 0000000..34892b5
--- /dev/null
@@ -0,0 +1,535 @@
+package org.argeo.cms.e4.users;
+
+import static org.argeo.api.acr.ldap.LdapAttr.cn;
+import static org.argeo.api.acr.ldap.LdapAttr.givenName;
+import static org.argeo.api.acr.ldap.LdapAttr.mail;
+import static org.argeo.api.acr.ldap.LdapAttr.sn;
+import static org.argeo.api.acr.ldap.LdapAttr.uid;
+import static org.argeo.cms.auth.UserAdminUtils.getProperty;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import org.argeo.api.acr.ldap.LdapAttr;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.cms.CurrentUser;
+import org.argeo.cms.auth.UserAdminUtils;
+import org.argeo.cms.e4.users.providers.CommonNameLP;
+import org.argeo.cms.e4.users.providers.DomainNameLP;
+import org.argeo.cms.e4.users.providers.RoleIconLP;
+import org.argeo.cms.e4.users.providers.UserFilter;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.swt.useradmin.LdifUsersTable;
+import org.argeo.cms.ui.eclipse.forms.AbstractFormPart;
+//import org.argeo.cms.ui.eclipse.forms.FormToolkit;
+import org.argeo.cms.ui.eclipse.forms.IManagedForm;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.TrayDialog;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerDropAdapter;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.ToolBar;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Display/edit the properties of a given user */
+public class UserEditor extends AbstractRoleEditor {
+       // final static String ID = "UserEditor.mainPage";
+
+       @Inject
+       private EPartService partService;
+
+       // private final UserEditor editor;
+       // private UserAdminWrapper userAdminWrapper;
+
+       // Local configuration
+       // private final int PRE_TITLE_INDENT = 10;
+
+       // public UserMainPage(FormEditor editor, UserAdminWrapper userAdminWrapper) {
+       // super(editor, ID, "Main");
+       // this.editor = (UserEditor) editor;
+       // this.userAdminWrapper = userAdminWrapper;
+       // }
+
+       // protected void createFormContent(final IManagedForm mf) {
+       // ScrolledForm form = mf.getForm();
+       // Composite body = form.getBody();
+       // GridLayout mainLayout = new GridLayout();
+       // // mainLayout.marginRight = 10;
+       // body.setLayout(mainLayout);
+       // User user = editor.getDisplayedUser();
+       // appendOverviewPart(body, user);
+       // // Remove to ability to force the password for his own user. The user
+       // // must then use the change pwd feature
+       // appendMemberOfPart(body, user);
+       // }
+
+       @Override
+       protected void createUi(Composite body) {
+               // Composite body = new Composite(parent, SWT.BORDER);
+               GridLayout mainLayout = new GridLayout();
+               // mainLayout.marginRight = 10;
+               body.setLayout(mainLayout);
+               // body.getParent().setLayout(new GridLayout());
+               // body.setLayoutData(CmsUiUtils.fillAll());
+               User user = getDisplayedUser();
+               appendOverviewPart(body, user);
+               // Remove to ability to force the password for his own user. The user
+               // must then use the change pwd feature
+               appendMemberOfPart(body, user);
+       }
+
+       /** Creates the general section */
+       private void appendOverviewPart(final Composite parent, final User user) {
+               // FormToolkit tk = getManagedForm().getToolkit();
+
+               // Section section = tk.createSection(parent, SWT.NO_FOCUS);
+               // GridData gd = EclipseUiUtils.fillWidth();
+               // // gd.verticalAlignment = PRE_TITLE_INDENT;
+               // section.setLayoutData(gd);
+               Composite body = new Composite(parent, SWT.NONE);
+               body.setLayoutData(EclipseUiUtils.fillWidth());
+               // section.setClient(body);
+               // body.setLayout(new GridLayout(6, false));
+               body.setLayout(new GridLayout(2, false));
+
+               Text commonName = createReadOnlyLT(body, "Name", getProperty(user, cn));
+               Text distinguishedName = createReadOnlyLT(body, "Login", getProperty(user, uid));
+               Text firstName = createLT(body, "First name", getProperty(user, givenName));
+               Text lastName = createLT(body, "Last name", getProperty(user, sn));
+               Text email = createLT(body, "Email", getProperty(user, mail));
+
+               Link resetPwdLk = new Link(body, SWT.NONE);
+               if (!UserAdminUtils.isCurrentUser(user)) {
+                       resetPwdLk.setText("<a>Reset password</a>");
+               }
+               resetPwdLk.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, 2, 1));
+
+               // create form part (controller)
+               AbstractFormPart part = new AbstractFormPart() {
+                       private MainInfoListener listener;
+
+                       @Override
+                       public void initialize(IManagedForm form) {
+                               super.initialize(form);
+                               listener = new MainInfoListener(parent.getDisplay(), this);
+                               userAdminWrapper.addListener(listener);
+                       }
+
+                       @Override
+                       public void dispose() {
+                               userAdminWrapper.removeListener(listener);
+                               super.dispose();
+                       }
+
+                       @SuppressWarnings("unchecked")
+                       public void commit(boolean onSave) {
+                               // TODO Sanity checks (mail validity...)
+                               user.getProperties().put(LdapAttr.givenName.name(), firstName.getText());
+                               user.getProperties().put(LdapAttr.sn.name(), lastName.getText());
+                               user.getProperties().put(LdapAttr.cn.name(), commonName.getText());
+                               user.getProperties().put(LdapAttr.mail.name(), email.getText());
+                               super.commit(onSave);
+                       }
+
+                       @Override
+                       public void refresh() {
+                               distinguishedName.setText(UserAdminUtils.getProperty(user, LdapAttr.uid.name()));
+                               commonName.setText(UserAdminUtils.getProperty(user, LdapAttr.cn.name()));
+                               firstName.setText(UserAdminUtils.getProperty(user, LdapAttr.givenName.name()));
+                               lastName.setText(UserAdminUtils.getProperty(user, LdapAttr.sn.name()));
+                               email.setText(UserAdminUtils.getProperty(user, LdapAttr.mail.name()));
+                               refreshFormTitle(user);
+                               super.refresh();
+                       }
+               };
+
+               // Improve this: automatically generate CN when first or last name
+               // changes
+               ModifyListener cnML = new ModifyListener() {
+                       private static final long serialVersionUID = 4298649222869835486L;
+
+                       @Override
+                       public void modifyText(ModifyEvent event) {
+                               String first = firstName.getText();
+                               String last = lastName.getText();
+                               String cn = first.trim() + " " + last.trim() + " ";
+                               cn = cn.trim();
+                               commonName.setText(cn);
+                               // getManagedForm().getForm().setText(cn);
+                               updateEditorTitle(cn);
+                       }
+               };
+               firstName.addModifyListener(cnML);
+               lastName.addModifyListener(cnML);
+
+               ModifyListener defaultListener = new FormPartML(part);
+               firstName.addModifyListener(defaultListener);
+               lastName.addModifyListener(defaultListener);
+               email.addModifyListener(defaultListener);
+
+               if (!UserAdminUtils.isCurrentUser(user))
+                       resetPwdLk.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = 5881800534589073787L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       new ChangePasswordDialog(user, "Reset password").open();
+                               }
+                       });
+
+               getManagedForm().addPart(part);
+       }
+
+       private class ChangePasswordDialog extends TrayDialog {
+               private static final long serialVersionUID = 2843538207460082349L;
+
+               private User user;
+               private Text password1;
+               private Text password2;
+               private String title;
+               // private FormToolkit tk;
+
+               public ChangePasswordDialog(User user, String title) {
+                       super(Display.getDefault().getActiveShell());
+                       // this.tk = tk;
+                       this.user = user;
+                       this.title = title;
+               }
+
+               protected Control createDialogArea(Composite parent) {
+                       Composite dialogarea = (Composite) super.createDialogArea(parent);
+                       dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       Composite body = new Composite(dialogarea, SWT.NO_FOCUS);
+                       body.setLayoutData(EclipseUiUtils.fillAll());
+                       GridLayout layout = new GridLayout(2, false);
+                       body.setLayout(layout);
+
+                       password1 = createLP(body, "New password", "");
+                       password2 = createLP(body, "Repeat password", "");
+                       parent.pack();
+                       return body;
+               }
+
+               @SuppressWarnings("unchecked")
+               @Override
+               protected void okPressed() {
+                       String msg = null;
+
+                       if (password1.getText().equals(""))
+                               msg = "Password cannot be empty";
+                       else if (password1.getText().equals(password2.getText())) {
+                               char[] newPassword = password1.getText().toCharArray();
+                               // userAdminWrapper.beginTransactionIfNeeded();
+                               userAdminWrapper.beginTransactionIfNeeded();
+                               user.getCredentials().put(null, newPassword);
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               super.okPressed();
+                       } else {
+                               msg = "Passwords are not equals";
+                       }
+
+                       if (EclipseUiUtils.notEmpty(msg))
+                               MessageDialog.openError(getParentShell(), "Cannot reset pasword", msg);
+               }
+
+               protected void configureShell(Shell shell) {
+                       super.configureShell(shell);
+                       shell.setText(title);
+               }
+       }
+
+       private LdifUsersTable appendMemberOfPart(final Composite parent, User user) {
+               // Section section = addSection(tk, parent, "Roles");
+               // Composite body = (Composite) section.getClient();
+               // Composite body= parent;
+               Composite body = new Composite(parent, SWT.BORDER);
+               body.setLayout(new GridLayout());
+               body.setLayoutData(CmsSwtUtils.fillAll());
+
+               // boolean isAdmin = CurrentUser.isInRole(NodeConstants.ROLE_ADMIN);
+
+               // Displayed columns
+               List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+               columnDefs.add(new ColumnDefinition(new RoleIconLP(), "", 0, 24));
+               columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Name", 150));
+               columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 100));
+               // Only show technical DN to administrators
+               // if (isAdmin)
+               // columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name",
+               // 300));
+
+               // Create and configure the table
+               final LdifUsersTable userViewerCmp = new MyUserTableViewer(body, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL, user);
+
+               userViewerCmp.setColumnDefinitions(columnDefs);
+               // if (isAdmin)
+               // userViewerCmp.populateWithStaticFilters(false, false);
+               // else
+               userViewerCmp.populate(true, false);
+               GridData gd = EclipseUiUtils.fillAll();
+               gd.heightHint = 500;
+               userViewerCmp.setLayoutData(gd);
+
+               // Controllers
+               TableViewer userViewer = userViewerCmp.getTableViewer();
+               userViewer.addDoubleClickListener(new UserTableDefaultDClickListener(partService));
+               int operations = DND.DROP_COPY | DND.DROP_MOVE;
+               Transfer[] tt = new Transfer[] { TextTransfer.getInstance() };
+               GroupDropListener dropL = new GroupDropListener(userAdminWrapper, userViewer, user);
+               userViewer.addDropSupport(operations, tt, dropL);
+
+               AbstractFormPart part = new AbstractFormPart() {
+
+                       private GroupChangeListener listener;
+
+                       @Override
+                       public void initialize(IManagedForm form) {
+                               super.initialize(form);
+                               listener = new GroupChangeListener(parent.getDisplay(), this);
+                               userAdminWrapper.addListener(listener);
+                       }
+
+                       public void commit(boolean onSave) {
+                               super.commit(onSave);
+                       }
+
+                       @Override
+                       public void dispose() {
+                               userAdminWrapper.removeListener(listener);
+                               super.dispose();
+                       }
+
+                       @Override
+                       public void refresh() {
+                               userViewerCmp.refresh();
+                               super.refresh();
+                       }
+               };
+               getManagedForm().addPart(part);
+               // addRemoveAbitily(body, userViewer, user);
+               // userViewerCmp.refresh();
+               String tooltip = "Remove " + UserAdminUtils.getUserLocalId(user.getName()) + " from the below selected groups";
+               Action action = new RemoveMembershipAction(userViewer, user, tooltip, SecurityAdminImages.ICON_REMOVE_DESC);
+               ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
+               ToolBar toolBar = toolBarManager.createControl(body);
+               toolBar.setLayoutData(CmsSwtUtils.fillWidth());
+               toolBarManager.add(action);
+               toolBarManager.update(true);
+               return userViewerCmp;
+       }
+
+       private class MyUserTableViewer extends LdifUsersTable {
+               private static final long serialVersionUID = 2653790051461237329L;
+
+               private Button showSystemRoleBtn;
+
+               private final User user;
+               private final UserFilter userFilter;
+
+               public MyUserTableViewer(Composite parent, int style, User user) {
+                       super(parent, style, true);
+                       this.user = user;
+                       userFilter = new UserFilter();
+               }
+
+               protected void populateStaticFilters(Composite staticFilterCmp) {
+                       staticFilterCmp.setLayout(new GridLayout());
+                       showSystemRoleBtn = new Button(staticFilterCmp, SWT.CHECK);
+                       showSystemRoleBtn.setText("Show system roles");
+                       boolean showSysRole = CurrentUser.isInRole(CmsConstants.ROLE_ADMIN);
+                       showSystemRoleBtn.setSelection(showSysRole);
+                       userFilter.setShowSystemRole(showSysRole);
+                       showSystemRoleBtn.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = -7033424592697691676L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       userFilter.setShowSystemRole(showSystemRoleBtn.getSelection());
+                                       refresh();
+                               }
+                       });
+               }
+
+               @Override
+               protected List<User> listFilteredElements(String filter) {
+                       List<User> users = (List<User>) getFlatGroups(null);
+                       List<User> filteredUsers = new ArrayList<User>();
+                       if (users.contains(user))
+                               users.remove(user);
+                       userFilter.setSearchText(filter);
+                       for (User user : users)
+                               if (userFilter.select(null, null, user))
+                                       filteredUsers.add(user);
+                       return filteredUsers;
+               }
+       }
+
+       // private void addRemoveAbility(Composite parent, TableViewer userViewer, User
+       // user) {
+       // // Section section = sectionPart.getSection();
+       // ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
+       // ToolBar toolbar = toolBarManager.createControl(parent);
+       // final Cursor handCursor = new Cursor(Display.getCurrent(), SWT.CURSOR_HAND);
+       // toolbar.setCursor(handCursor);
+       // toolbar.addDisposeListener(new DisposeListener() {
+       // private static final long serialVersionUID = 3882131405820522925L;
+       //
+       // public void widgetDisposed(DisposeEvent e) {
+       // if ((handCursor != null) && (handCursor.isDisposed() == false)) {
+       // handCursor.dispose();
+       // }
+       // }
+       // });
+       //
+       // String tooltip = "Remove " + UserAdminUtils.getUserLocalId(user.getName()) +
+       // " from the below selected groups";
+       // Action action = new RemoveMembershipAction(userViewer, user, tooltip,
+       // SecurityAdminImages.ICON_REMOVE_DESC);
+       // toolBarManager.add(action);
+       // toolBarManager.update(true);
+       // // section.setTextClient(toolbar);
+       // }
+
+       private class RemoveMembershipAction extends Action {
+               private static final long serialVersionUID = -1337713097184522588L;
+
+               private final TableViewer userViewer;
+               private final User user;
+
+               RemoveMembershipAction(TableViewer userViewer, User user, String name, ImageDescriptor img) {
+                       super(name, img);
+                       this.userViewer = userViewer;
+                       this.user = user;
+               }
+
+               @Override
+               public void run() {
+                       ISelection selection = userViewer.getSelection();
+                       if (selection.isEmpty())
+                               return;
+
+                       @SuppressWarnings("unchecked")
+                       Iterator<Group> it = ((IStructuredSelection) selection).iterator();
+                       List<Group> groups = new ArrayList<Group>();
+                       while (it.hasNext()) {
+                               Group currGroup = it.next();
+                               groups.add(currGroup);
+                       }
+
+                       userAdminWrapper.beginTransactionIfNeeded();
+                       for (Group group : groups) {
+                               group.removeMember(user);
+                       }
+                       userAdminWrapper.commitOrNotifyTransactionStateChange();
+                       for (Group group : groups) {
+                               userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, group));
+                       }
+               }
+       }
+
+       /**
+        * Defines the table as being a potential target to add group memberships
+        * (roles) to this user
+        */
+       private class GroupDropListener extends ViewerDropAdapter {
+               private static final long serialVersionUID = 2893468717831451621L;
+
+               private final UserAdminWrapper myUserAdminWrapper;
+               private final User myUser;
+
+               public GroupDropListener(UserAdminWrapper userAdminWrapper, Viewer userViewer, User user) {
+                       super(userViewer);
+                       this.myUserAdminWrapper = userAdminWrapper;
+                       this.myUser = user;
+               }
+
+               @Override
+               public boolean validateDrop(Object target, int operation, TransferData transferType) {
+                       // Target is always OK in a list only view
+                       // TODO check if not a string
+                       boolean validDrop = true;
+                       return validDrop;
+               }
+
+               @Override
+               public void drop(DropTargetEvent event) {
+                       String name = (String) event.data;
+                       UserAdmin myUserAdmin = myUserAdminWrapper.getUserAdmin();
+                       Role role = myUserAdmin.getRole(name);
+                       // TODO this check should be done before.
+                       if (role.getType() == Role.GROUP) {
+                               // TODO check if the user is already member of this group
+
+                               myUserAdminWrapper.beginTransactionIfNeeded();
+                               Group group = (Group) role;
+                               group.addMember(myUser);
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               myUserAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, group));
+                       }
+                       super.drop(event);
+               }
+
+               @Override
+               public boolean performDrop(Object data) {
+                       // userTableViewerCmp.refresh();
+                       return true;
+               }
+       }
+
+       // LOCAL HELPERS
+       private void refreshFormTitle(User group) {
+               // getManagedForm().getForm().setText(UserAdminUtils.getProperty(group,
+               // LdapAttrs.cn.name()));
+       }
+
+       /** Appends a section with a title */
+       // private Section addSection(FormToolkit tk, Composite parent, String title) {
+       // Section section = tk.createSection(parent, Section.TITLE_BAR);
+       // GridData gd = EclipseUiUtils.fillWidth();
+       // gd.verticalAlignment = PRE_TITLE_INDENT;
+       // section.setLayoutData(gd);
+       // section.setText(title);
+       // // section.getMenu().setVisible(true);
+       //
+       // Composite body = tk.createComposite(section, SWT.WRAP);
+       // body.setLayoutData(EclipseUiUtils.fillAll());
+       // section.setClient(body);
+       //
+       // return section;
+       // }
+
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserTableDefaultDClickListener.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserTableDefaultDClickListener.java
new file mode 100644 (file)
index 0000000..c3e6b0c
--- /dev/null
@@ -0,0 +1,39 @@
+package org.argeo.cms.e4.users;
+
+import org.argeo.api.acr.ldap.LdapAttr;
+import org.argeo.cms.e4.CmsE4Utils;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Default double click listener for the various user tables, will open the
+ * clicked item in the editor
+ */
+public class UserTableDefaultDClickListener implements IDoubleClickListener {
+       private final EPartService partService;
+
+       public UserTableDefaultDClickListener(EPartService partService) {
+               this.partService = partService;
+       }
+
+       public void doubleClick(DoubleClickEvent evt) {
+               if (evt.getSelection().isEmpty())
+                       return;
+               Object obj = ((IStructuredSelection) evt.getSelection()).getFirstElement();
+               User user = (User) obj;
+
+               String editorId = getEditorId(user);
+               CmsE4Utils.openEditor(partService, editorId, LdapAttr.uid.name(), user.getName());
+       }
+
+       protected String getEditorId(User user) {
+               if (user instanceof Group)
+                       return "org.argeo.cms.e4.partdescriptor.groupEditor";
+               else
+                       return "org.argeo.cms.e4.partdescriptor.userEditor";
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UsersView.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UsersView.java
new file mode 100644 (file)
index 0000000..238ae4d
--- /dev/null
@@ -0,0 +1,182 @@
+package org.argeo.cms.e4.users;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+
+import org.argeo.api.acr.ldap.LdapAttr;
+import org.argeo.api.acr.ldap.LdapObj;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.cms.CurrentUser;
+import org.argeo.cms.e4.users.providers.CommonNameLP;
+import org.argeo.cms.e4.users.providers.DomainNameLP;
+import org.argeo.cms.e4.users.providers.MailLP;
+import org.argeo.cms.e4.users.providers.UserDragListener;
+import org.argeo.cms.e4.users.providers.UserNameLP;
+import org.argeo.cms.swt.CmsException;
+import org.argeo.cms.swt.useradmin.LdifUsersTable;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.e4.ui.di.Focus;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/** List all users with filter - based on Ldif userAdmin */
+public class UsersView {
+       // private final static Log log = LogFactory.getLog(UsersView.class);
+
+       // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".usersView";
+
+       @Inject
+       private UserAdminWrapper userAdminWrapper;
+       @Inject
+       private EPartService partService;
+
+       // UI Objects
+       private LdifUsersTable userTableViewerCmp;
+       private TableViewer userViewer;
+       private List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+
+       private UserAdminListener listener;
+
+       @PostConstruct
+       public void createPartControl(Composite parent, ESelectionService selectionService) {
+
+               parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+               // Define the displayed columns
+               columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Common Name", 150));
+               columnDefs.add(new ColumnDefinition(new MailLP(), "E-mail", 150));
+               columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 200));
+               // Only show technical DN to admin
+               if (CurrentUser.isInRole(CmsConstants.ROLE_ADMIN))
+                       columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name", 300));
+
+               // Create and configure the table
+               userTableViewerCmp = new MyUserTableViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+               userTableViewerCmp.setLayoutData(EclipseUiUtils.fillAll());
+               userTableViewerCmp.setColumnDefinitions(columnDefs);
+               userTableViewerCmp.populate(true, false);
+
+               // Links
+               userViewer = userTableViewerCmp.getTableViewer();
+               userViewer.addDoubleClickListener(new UserTableDefaultDClickListener(partService));
+               userViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+
+                       @Override
+                       public void selectionChanged(SelectionChangedEvent event) {
+                               IStructuredSelection selection = (IStructuredSelection) event.getSelection();
+                               selectionService.setSelection(selection.toList());
+                       }
+               });
+               // getViewSite().setSelectionProvider(userViewer);
+
+               // Really?
+               userTableViewerCmp.refresh();
+
+               // Drag and drop
+               int operations = DND.DROP_COPY | DND.DROP_MOVE;
+               Transfer[] tt = new Transfer[] { TextTransfer.getInstance() };
+               userViewer.addDragSupport(operations, tt, new UserDragListener(userViewer));
+
+               // Register a useradmin listener
+               listener = new MyUiUAListener(parent.getDisplay());
+               userAdminWrapper.addListener(listener);
+       }
+
+       private class MyUiUAListener extends UiUserAdminListener {
+               public MyUiUAListener(Display display) {
+                       super(display);
+               }
+
+               @Override
+               public void roleChangedToUiThread(UserAdminEvent event) {
+                       if (userViewer != null && !userViewer.getTable().isDisposed())
+                               refresh();
+               }
+       }
+
+       private class MyUserTableViewer extends LdifUsersTable {
+               private static final long serialVersionUID = 8467999509931900367L;
+
+               private final String[] knownProps = { LdapAttr.DN, LdapAttr.uid.name(), LdapAttr.cn.name(),
+                               LdapAttr.givenName.name(), LdapAttr.sn.name(), LdapAttr.mail.name() };
+
+               public MyUserTableViewer(Composite parent, int style) {
+                       super(parent, style);
+               }
+
+               @Override
+               protected List<User> listFilteredElements(String filter) {
+                       Role[] roles;
+
+                       try {
+                               StringBuilder builder = new StringBuilder();
+
+                               StringBuilder tmpBuilder = new StringBuilder();
+                               if (EclipseUiUtils.notEmpty(filter))
+                                       for (String prop : knownProps) {
+                                               tmpBuilder.append("(");
+                                               tmpBuilder.append(prop);
+                                               tmpBuilder.append("=*");
+                                               tmpBuilder.append(filter);
+                                               tmpBuilder.append("*)");
+                                       }
+                               if (tmpBuilder.length() > 1) {
+                                       builder.append("(&(").append(LdapAttr.objectClass.name()).append("=")
+                                                       .append(LdapObj.inetOrgPerson.name()).append(")(|");
+                                       builder.append(tmpBuilder.toString());
+                                       builder.append("))");
+                               } else
+                                       builder.append("(").append(LdapAttr.objectClass.name()).append("=")
+                                                       .append(LdapObj.inetOrgPerson.name()).append(")");
+                               roles = userAdminWrapper.getUserAdmin().getRoles(builder.toString());
+                       } catch (InvalidSyntaxException e) {
+                               throw new CmsException("Unable to get roles with filter: " + filter, e);
+                       }
+                       List<User> users = new ArrayList<User>();
+                       for (Role role : roles)
+                               // if (role.getType() == Role.USER && role.getType() !=
+                               // Role.GROUP)
+                               users.add((User) role);
+                       return users;
+               }
+       }
+
+       public void refresh() {
+               userTableViewerCmp.refresh();
+       }
+
+       // Override generic view methods
+       @PreDestroy
+       public void dispose() {
+               userAdminWrapper.removeListener(listener);
+       }
+
+       @Focus
+       public void setFocus() {
+               userTableViewerCmp.setFocus();
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/DeleteGroups.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/DeleteGroups.java
new file mode 100644 (file)
index 0000000..742bc3f
--- /dev/null
@@ -0,0 +1,95 @@
+package org.argeo.cms.e4.users.handlers;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.argeo.cms.auth.UserAdminUtils;
+import org.argeo.cms.e4.users.GroupsView;
+import org.argeo.cms.e4.users.UserAdminWrapper;
+import org.eclipse.e4.core.di.annotations.CanExecute;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.services.IServiceConstants;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.widgets.Display;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Delete the selected groups */
+public class DeleteGroups {
+       // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID +
+       // ".deleteGroups";
+
+       /* DEPENDENCY INJECTION */
+       @Inject
+       private UserAdminWrapper userAdminWrapper;
+
+       @Inject
+       ESelectionService selectionService;
+
+       @SuppressWarnings("unchecked")
+       @Execute
+       public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) {
+               // ISelection selection = null;// HandlerUtil.getCurrentSelection(event);
+               // if (selection.isEmpty())
+               // return null;
+               //
+               // List<Group> groups = new ArrayList<Group>();
+               // Iterator<Group> it = ((IStructuredSelection) selection).iterator();
+
+               List<Group> selection = (List<Group>) selectionService.getSelection();
+               if (selection == null)
+                       return;
+
+               StringBuilder builder = new StringBuilder();
+               for (Group group : selection) {
+                       Group currGroup = group;
+                       String groupName = UserAdminUtils.getUserLocalId(currGroup.getName());
+                       // TODO add checks
+                       builder.append(groupName).append("; ");
+                       // groups.add(currGroup);
+               }
+
+               if (!MessageDialog.openQuestion(Display.getCurrent().getActiveShell(), "Delete Groups", "Are you sure that you "
+                               + "want to delete these groups?\n" + builder.substring(0, builder.length() - 2)))
+                       return;
+
+               userAdminWrapper.beginTransactionIfNeeded();
+               UserAdmin userAdmin = userAdminWrapper.getUserAdmin();
+               // IWorkbenchPage iwp =
+               // HandlerUtil.getActiveWorkbenchWindow(event).getActivePage();
+               for (Group group : selection) {
+                       String groupName = group.getName();
+                       // TODO find a way to close the editor cleanly if opened. Cannot be
+                       // done through the UserAdminListeners, it causes a
+                       // java.util.ConcurrentModificationException because disposing the
+                       // editor unregisters and disposes the listener
+                       // IEditorPart part = iwp.findEditor(new UserEditorInput(groupName));
+                       // if (part != null)
+                       // iwp.closeEditor(part, false);
+                       userAdmin.removeRole(groupName);
+               }
+               userAdminWrapper.commitOrNotifyTransactionStateChange();
+
+               // Update the view
+               for (Group group : selection) {
+                       userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_REMOVED, group));
+               }
+
+               // return null;
+       }
+
+       @CanExecute
+       public boolean canExecute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) {
+               return part.getObject() instanceof GroupsView && selectionService.getSelection() != null;
+       }
+
+       /* DEPENDENCY INJECTION */
+       // public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+       // this.userAdminWrapper = userAdminWrapper;
+       // }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/DeleteUsers.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/DeleteUsers.java
new file mode 100644 (file)
index 0000000..d1afd22
--- /dev/null
@@ -0,0 +1,88 @@
+package org.argeo.cms.e4.users.handlers;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.argeo.cms.auth.UserAdminUtils;
+import org.argeo.cms.e4.users.UserAdminWrapper;
+import org.argeo.cms.e4.users.UsersView;
+import org.eclipse.e4.core.di.annotations.CanExecute;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.services.IServiceConstants;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.widgets.Display;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Delete the selected users */
+public class DeleteUsers {
+       // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".deleteUsers";
+
+       /* DEPENDENCY INJECTION */
+       @Inject
+       private UserAdminWrapper userAdminWrapper;
+
+       @SuppressWarnings("unchecked")
+       @Execute
+       public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) {
+               // ISelection selection = null;// HandlerUtil.getCurrentSelection(event);
+               // if (selection.isEmpty())
+               // return null;
+               List<User> selection = (List<User>) selectionService.getSelection();
+               if (selection == null)
+                       return;
+
+//             Iterator<User> it = ((IStructuredSelection) selection).iterator();
+//             List<User> users = new ArrayList<User>();
+               StringBuilder builder = new StringBuilder();
+
+               for(User user:selection) {
+                       User currUser = user;
+//                     User currUser = it.next();
+                       String userName = UserAdminUtils.getUserLocalId(currUser.getName());
+                       if (UserAdminUtils.isCurrentUser(currUser)) {
+                               MessageDialog.openError(Display.getCurrent().getActiveShell(), "Deletion forbidden",
+                                               "You cannot delete your own user this way.");
+                               return;
+                       }
+                       builder.append(userName).append("; ");
+//                     users.add(currUser);
+               }
+
+               if (!MessageDialog.openQuestion(Display.getCurrent().getActiveShell(), "Delete Users",
+                               "Are you sure that you want to delete these users?\n" + builder.substring(0, builder.length() - 2)))
+                       return;
+
+               userAdminWrapper.beginTransactionIfNeeded();
+               UserAdmin userAdmin = userAdminWrapper.getUserAdmin();
+               // IWorkbenchPage iwp =
+               // HandlerUtil.getActiveWorkbenchWindow(event).getActivePage();
+
+               for (User user : selection) {
+                       String userName = user.getName();
+                       // TODO find a way to close the editor cleanly if opened. Cannot be
+                       // done through the UserAdminListeners, it causes a
+                       // java.util.ConcurrentModificationException because disposing the
+                       // editor unregisters and disposes the listener
+                       // IEditorPart part = iwp.findEditor(new UserEditorInput(userName));
+                       // if (part != null)
+                       // iwp.closeEditor(part, false);
+                       userAdmin.removeRole(userName);
+               }
+               userAdminWrapper.commitOrNotifyTransactionStateChange();
+
+               for (User user : selection) {
+                       userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_REMOVED, user));
+               }
+       }
+
+       @CanExecute
+       public boolean canExecute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) {
+               return part.getObject() instanceof UsersView && selectionService.getSelection() != null;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/NewGroup.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/NewGroup.java
new file mode 100644 (file)
index 0000000..5b41eae
--- /dev/null
@@ -0,0 +1,212 @@
+package org.argeo.cms.e4.users.handlers;
+
+import java.util.Dictionary;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.argeo.api.acr.ldap.LdapAttr;
+import org.argeo.cms.e4.users.UserAdminWrapper;
+import org.argeo.cms.runtime.DirectoryConf;
+import org.argeo.cms.swt.CmsException;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Create a new group */
+public class NewGroup {
+       // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".newGroup";
+
+       /* DEPENDENCY INJECTION */
+       @Inject
+       private UserAdminWrapper userAdminWrapper;
+
+       @Execute
+       public Object execute() {
+               NewGroupWizard newGroupWizard = new NewGroupWizard();
+               newGroupWizard.setWindowTitle("Group creation");
+               WizardDialog dialog = new WizardDialog(Display.getCurrent().getActiveShell(), newGroupWizard);
+               dialog.open();
+               return null;
+       }
+
+       private class NewGroupWizard extends Wizard {
+
+               // Pages
+               private MainGroupInfoWizardPage mainGroupInfo;
+
+               // UI fields
+               private Text dNameTxt, commonNameTxt, descriptionTxt;
+               private Combo baseDnCmb;
+
+               public NewGroupWizard() {
+               }
+
+               @Override
+               public void addPages() {
+                       mainGroupInfo = new MainGroupInfoWizardPage();
+                       addPage(mainGroupInfo);
+               }
+
+               @SuppressWarnings({ "rawtypes", "unchecked" })
+               @Override
+               public boolean performFinish() {
+                       if (!canFinish())
+                               return false;
+                       String commonName = commonNameTxt.getText();
+                       try {
+                               userAdminWrapper.beginTransactionIfNeeded();
+                               String dn = getDn(commonName);
+                               Group group = (Group) userAdminWrapper.getUserAdmin().createRole(dn, Role.GROUP);
+                               Dictionary props = group.getProperties();
+                               String descStr = descriptionTxt.getText();
+                               if (EclipseUiUtils.notEmpty(descStr))
+                                       props.put(LdapAttr.description.name(), descStr);
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CREATED, group));
+                               return true;
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Cannot create new group " + commonName, e);
+                               return false;
+                       }
+               }
+
+               private class MainGroupInfoWizardPage extends WizardPage implements FocusListener {
+                       private static final long serialVersionUID = -3150193365151601807L;
+
+                       public MainGroupInfoWizardPage() {
+                               super("Main");
+                               setTitle("General information");
+                               setMessage("Please choose a domain, provide a common name " + "and a free description");
+                       }
+
+                       @Override
+                       public void createControl(Composite parent) {
+                               Composite bodyCmp = new Composite(parent, SWT.NONE);
+                               setControl(bodyCmp);
+                               bodyCmp.setLayout(new GridLayout(2, false));
+
+                               dNameTxt = EclipseUiUtils.createGridLT(bodyCmp, "Distinguished name");
+                               dNameTxt.setEnabled(false);
+
+                               baseDnCmb = createGridLC(bodyCmp, "Base DN");
+                               // Initialise before adding the listener to avoid NPE
+                               initialiseDnCmb(baseDnCmb);
+                               baseDnCmb.addFocusListener(this);
+
+                               commonNameTxt = EclipseUiUtils.createGridLT(bodyCmp, "Common name");
+                               commonNameTxt.addFocusListener(this);
+
+                               Label descLbl = new Label(bodyCmp, SWT.LEAD);
+                               descLbl.setText("Description");
+                               descLbl.setLayoutData(new GridData(SWT.RIGHT, SWT.TOP, false, false));
+                               descriptionTxt = new Text(bodyCmp, SWT.LEAD | SWT.MULTI | SWT.WRAP | SWT.BORDER);
+                               descriptionTxt.setLayoutData(EclipseUiUtils.fillAll());
+                               descriptionTxt.addFocusListener(this);
+
+                               // Initialize buttons
+                               setPageComplete(false);
+                               getContainer().updateButtons();
+                       }
+
+                       @Override
+                       public void focusLost(FocusEvent event) {
+                               String name = commonNameTxt.getText();
+                               if (EclipseUiUtils.isEmpty(name))
+                                       dNameTxt.setText("");
+                               else
+                                       dNameTxt.setText(getDn(name));
+
+                               String message = checkComplete();
+                               if (message != null) {
+                                       setMessage(message, WizardPage.ERROR);
+                                       setPageComplete(false);
+                               } else {
+                                       setMessage("Complete", WizardPage.INFORMATION);
+                                       setPageComplete(true);
+                               }
+                               getContainer().updateButtons();
+                       }
+
+                       @Override
+                       public void focusGained(FocusEvent event) {
+                       }
+
+                       /** @return the error message or null if complete */
+                       protected String checkComplete() {
+                               String name = commonNameTxt.getText();
+
+                               if (name.trim().equals(""))
+                                       return "Common name must not be empty";
+                               Role role = userAdminWrapper.getUserAdmin().getRole(getDn(name));
+                               if (role != null)
+                                       return "Group " + name + " already exists";
+                               return null;
+                       }
+
+                       @Override
+                       public void setVisible(boolean visible) {
+                               super.setVisible(visible);
+                               if (visible)
+                                       if (baseDnCmb.getSelectionIndex() == -1)
+                                               baseDnCmb.setFocus();
+                                       else
+                                               commonNameTxt.setFocus();
+                       }
+               }
+
+               private Map<String, String> getDns() {
+                       return userAdminWrapper.getKnownBaseDns(true);
+               }
+
+               private String getDn(String cn) {
+                       Map<String, String> dns = getDns();
+                       String bdn = baseDnCmb.getText();
+                       if (EclipseUiUtils.notEmpty(bdn)) {
+                               Dictionary<String, ?> props = DirectoryConf.uriAsProperties(dns.get(bdn));
+                               String dn = LdapAttr.cn.name() + "=" + cn + "," + DirectoryConf.groupBase.getValue(props) + "," + bdn;
+                               return dn;
+                       }
+                       return null;
+               }
+
+               private void initialiseDnCmb(Combo combo) {
+                       Map<String, String> dns = userAdminWrapper.getKnownBaseDns(true);
+                       if (dns.isEmpty())
+                               throw new CmsException("No writable base dn found. Cannot create group");
+                       combo.setItems(dns.keySet().toArray(new String[0]));
+                       if (dns.size() == 1)
+                               combo.select(0);
+               }
+       }
+
+       private Combo createGridLC(Composite parent, String label) {
+               Label lbl = new Label(parent, SWT.LEAD);
+               lbl.setText(label);
+               lbl.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));
+               Combo combo = new Combo(parent, SWT.LEAD | SWT.BORDER | SWT.READ_ONLY);
+               combo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+               return combo;
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/NewUser.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/NewUser.java
new file mode 100644 (file)
index 0000000..d8fb0fe
--- /dev/null
@@ -0,0 +1,287 @@
+package org.argeo.cms.e4.users.handlers;
+
+import java.util.Dictionary;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+import org.argeo.api.acr.ldap.LdapAttr;
+import org.argeo.cms.auth.UserAdminUtils;
+import org.argeo.cms.e4.users.UiAdminUtils;
+import org.argeo.cms.e4.users.UserAdminWrapper;
+import org.argeo.cms.runtime.DirectoryConf;
+import org.argeo.cms.swt.CmsException;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Open a wizard that enables creation of a new user. */
+public class NewUser {
+       // private final static Log log = LogFactory.getLog(NewUser.class);
+       // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".newUser";
+
+       /* DEPENDENCY INJECTION */
+       @Inject
+       private UserAdminWrapper userAdminWrapper;
+
+       @Execute
+       public Object execute() {
+               NewUserWizard newUserWizard = new NewUserWizard();
+               newUserWizard.setWindowTitle("User creation");
+               WizardDialog dialog = new WizardDialog(Display.getCurrent().getActiveShell(), newUserWizard);
+               dialog.open();
+               return null;
+       }
+
+       private class NewUserWizard extends Wizard {
+
+               // pages
+               private MainUserInfoWizardPage mainUserInfo;
+
+               // End user fields
+               private Text dNameTxt, usernameTxt, firstNameTxt, lastNameTxt, primaryMailTxt, pwd1Txt, pwd2Txt;
+               private Combo baseDnCmb;
+
+               public NewUserWizard() {
+
+               }
+
+               @Override
+               public void addPages() {
+                       mainUserInfo = new MainUserInfoWizardPage();
+                       addPage(mainUserInfo);
+                       String message = "Default wizard that also eases user creation tests:\n "
+                                       + "Mail and last name are automatically "
+                                       + "generated form the uid. Password are defauted to 'demo'.";
+                       mainUserInfo.setMessage(message, WizardPage.WARNING);
+               }
+
+               @SuppressWarnings({ "rawtypes", "unchecked" })
+               @Override
+               public boolean performFinish() {
+                       if (!canFinish())
+                               return false;
+                       String username = mainUserInfo.getUsername();
+                       userAdminWrapper.beginTransactionIfNeeded();
+                       try {
+                               User user = (User) userAdminWrapper.getUserAdmin().createRole(getDn(username), Role.USER);
+
+                               Dictionary props = user.getProperties();
+
+                               String lastNameStr = lastNameTxt.getText();
+                               if (EclipseUiUtils.notEmpty(lastNameStr))
+                                       props.put(LdapAttr.sn.name(), lastNameStr);
+
+                               String firstNameStr = firstNameTxt.getText();
+                               if (EclipseUiUtils.notEmpty(firstNameStr))
+                                       props.put(LdapAttr.givenName.name(), firstNameStr);
+
+                               String cn = UserAdminUtils.buildDefaultCn(firstNameStr, lastNameStr);
+                               if (EclipseUiUtils.notEmpty(cn))
+                                       props.put(LdapAttr.cn.name(), cn);
+
+                               String mailStr = primaryMailTxt.getText();
+                               if (EclipseUiUtils.notEmpty(mailStr))
+                                       props.put(LdapAttr.mail.name(), mailStr);
+
+                               char[] password = mainUserInfo.getPassword();
+                               user.getCredentials().put(null, password);
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CREATED, user));
+                               return true;
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Cannot create new user " + username, e);
+                               return false;
+                       }
+               }
+
+               private class MainUserInfoWizardPage extends WizardPage implements ModifyListener {
+                       private static final long serialVersionUID = -3150193365151601807L;
+
+                       public MainUserInfoWizardPage() {
+                               super("Main");
+                               setTitle("Required Information");
+                       }
+
+                       @Override
+                       public void createControl(Composite parent) {
+                               Composite composite = new Composite(parent, SWT.NONE);
+                               composite.setLayout(new GridLayout(2, false));
+                               dNameTxt = EclipseUiUtils.createGridLT(composite, "Distinguished name", this);
+                               dNameTxt.setEnabled(false);
+
+                               baseDnCmb = createGridLC(composite, "Base DN");
+                               initialiseDnCmb(baseDnCmb);
+                               baseDnCmb.addModifyListener(this);
+                               baseDnCmb.addModifyListener(new ModifyListener() {
+                                       private static final long serialVersionUID = -1435351236582736843L;
+
+                                       @Override
+                                       public void modifyText(ModifyEvent event) {
+                                               String name = usernameTxt.getText();
+                                               dNameTxt.setText(getDn(name));
+                                       }
+                               });
+
+                               usernameTxt = EclipseUiUtils.createGridLT(composite, "Local ID", this);
+                               usernameTxt.addModifyListener(new ModifyListener() {
+                                       private static final long serialVersionUID = -1435351236582736843L;
+
+                                       @Override
+                                       public void modifyText(ModifyEvent event) {
+                                               String name = usernameTxt.getText();
+                                               if (name.trim().equals("")) {
+                                                       dNameTxt.setText("");
+                                                       lastNameTxt.setText("");
+                                                       primaryMailTxt.setText("");
+                                                       pwd1Txt.setText("");
+                                                       pwd2Txt.setText("");
+                                               } else {
+                                                       dNameTxt.setText(getDn(name));
+                                                       lastNameTxt.setText(name.toUpperCase());
+                                                       primaryMailTxt.setText(getMail(name));
+                                                       pwd1Txt.setText("demo");
+                                                       pwd2Txt.setText("demo");
+                                               }
+                                       }
+                               });
+
+                               primaryMailTxt = EclipseUiUtils.createGridLT(composite, "Email", this);
+                               firstNameTxt = EclipseUiUtils.createGridLT(composite, "First name", this);
+                               lastNameTxt = EclipseUiUtils.createGridLT(composite, "Last name", this);
+                               pwd1Txt = EclipseUiUtils.createGridLP(composite, "Password", this);
+                               pwd2Txt = EclipseUiUtils.createGridLP(composite, "Repeat password", this);
+                               setControl(composite);
+
+                               // Initialize buttons
+                               setPageComplete(false);
+                               getContainer().updateButtons();
+                       }
+
+                       @Override
+                       public void modifyText(ModifyEvent event) {
+                               String message = checkComplete();
+                               if (message != null) {
+                                       setMessage(message, WizardPage.ERROR);
+                                       setPageComplete(false);
+                               } else {
+                                       setMessage("Complete", WizardPage.INFORMATION);
+                                       setPageComplete(true);
+                               }
+                               getContainer().updateButtons();
+                       }
+
+                       /** @return error message or null if complete */
+                       protected String checkComplete() {
+                               String name = usernameTxt.getText();
+
+                               if (name.trim().equals(""))
+                                       return "User name must not be empty";
+                               Role role = userAdminWrapper.getUserAdmin().getRole(getDn(name));
+                               if (role != null)
+                                       return "User " + name + " already exists";
+                               if (!primaryMailTxt.getText().matches(UiAdminUtils.EMAIL_PATTERN))
+                                       return "Not a valid email address";
+                               if (lastNameTxt.getText().trim().equals(""))
+                                       return "Specify a last name";
+                               if (pwd1Txt.getText().trim().equals(""))
+                                       return "Specify a password";
+                               if (pwd2Txt.getText().trim().equals(""))
+                                       return "Repeat the password";
+                               if (!pwd2Txt.getText().equals(pwd1Txt.getText()))
+                                       return "Passwords are different";
+                               return null;
+                       }
+
+                       @Override
+                       public void setVisible(boolean visible) {
+                               super.setVisible(visible);
+                               if (visible)
+                                       if (baseDnCmb.getSelectionIndex() == -1)
+                                               baseDnCmb.setFocus();
+                                       else
+                                               usernameTxt.setFocus();
+                       }
+
+                       public String getUsername() {
+                               return usernameTxt.getText();
+                       }
+
+                       public char[] getPassword() {
+                               return pwd1Txt.getTextChars();
+                       }
+
+               }
+
+               private Map<String, String> getDns() {
+                       return userAdminWrapper.getKnownBaseDns(true);
+               }
+
+               private String getDn(String uid) {
+                       Map<String, String> dns = getDns();
+                       String bdn = baseDnCmb.getText();
+                       if (EclipseUiUtils.notEmpty(bdn)) {
+                               Dictionary<String, ?> props = DirectoryConf.uriAsProperties(dns.get(bdn));
+                               String dn = LdapAttr.uid.name() + "=" + uid + "," + DirectoryConf.userBase.getValue(props) + "," + bdn;
+                               return dn;
+                       }
+                       return null;
+               }
+
+               private void initialiseDnCmb(Combo combo) {
+                       Map<String, String> dns = userAdminWrapper.getKnownBaseDns(true);
+                       if (dns.isEmpty())
+                               throw new CmsException("No writable base dn found. Cannot create user");
+                       combo.setItems(dns.keySet().toArray(new String[0]));
+                       if (dns.size() == 1)
+                               combo.select(0);
+               }
+
+               private String getMail(String username) {
+                       if (baseDnCmb.getSelectionIndex() == -1)
+                               return null;
+                       String baseDn = baseDnCmb.getText();
+                       try {
+                               LdapName name = new LdapName(baseDn);
+                               List<Rdn> rdns = name.getRdns();
+                               return username + "@" + (String) rdns.get(1).getValue() + '.' + (String) rdns.get(0).getValue();
+                       } catch (InvalidNameException e) {
+                               throw new CmsException("Unable to generate mail for " + username + " with base dn " + baseDn, e);
+                       }
+               }
+       }
+
+       private Combo createGridLC(Composite parent, String label) {
+               Label lbl = new Label(parent, SWT.LEAD);
+               lbl.setText(label);
+               lbl.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));
+               Combo combo = new Combo(parent, SWT.LEAD | SWT.BORDER | SWT.READ_ONLY);
+               combo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+               return combo;
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/package-info.java
new file mode 100644 (file)
index 0000000..cf3db1d
--- /dev/null
@@ -0,0 +1,2 @@
+/** Users management handlers. */
+package org.argeo.cms.e4.users.handlers;
\ No newline at end of file
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/package-info.java
new file mode 100644 (file)
index 0000000..c6f14b0
--- /dev/null
@@ -0,0 +1,2 @@
+/** Users management perspective. */
+package org.argeo.cms.e4.users;
\ No newline at end of file
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/CommonNameLP.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/CommonNameLP.java
new file mode 100644 (file)
index 0000000..fb76f71
--- /dev/null
@@ -0,0 +1,21 @@
+package org.argeo.cms.e4.users.providers;
+
+import org.argeo.api.acr.ldap.LdapAttr;
+import org.argeo.cms.auth.UserAdminUtils;
+import org.osgi.service.useradmin.User;
+
+/** Simply declare a label provider that returns the common name of a user */
+public class CommonNameLP extends UserAdminAbstractLP {
+       private static final long serialVersionUID = 5256703081044911941L;
+
+       @Override
+       public String getText(User user) {
+               return UserAdminUtils.getProperty(user, LdapAttr.cn.name());
+       }
+
+       @Override
+       public String getToolTipText(Object element) {
+               return UserAdminUtils.getProperty((User) element, LdapAttr.DN);
+       }
+
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/DomainNameLP.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/DomainNameLP.java
new file mode 100644 (file)
index 0000000..e23729d
--- /dev/null
@@ -0,0 +1,14 @@
+package org.argeo.cms.e4.users.providers;
+
+import org.argeo.cms.auth.UserAdminUtils;
+import org.osgi.service.useradmin.User;
+
+/** The human friendly domain name for the corresponding user. */
+public class DomainNameLP extends UserAdminAbstractLP {
+       private static final long serialVersionUID = 5256703081044911941L;
+
+       @Override
+       public String getText(User user) {
+               return UserAdminUtils.getDomainName(user);
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/MailLP.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/MailLP.java
new file mode 100644 (file)
index 0000000..fe17a09
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.cms.e4.users.providers;
+
+import org.argeo.api.acr.ldap.LdapAttr;
+import org.argeo.cms.auth.UserAdminUtils;
+import org.osgi.service.useradmin.User;
+
+/** Simply declare a label provider that returns the Primary Mail of a user */
+public class MailLP extends UserAdminAbstractLP {
+       private static final long serialVersionUID = 8329764452141982707L;
+
+       @Override
+       public String getText(User user) {
+               return UserAdminUtils.getProperty(user, LdapAttr.mail.name());
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/RoleIconLP.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/RoleIconLP.java
new file mode 100644 (file)
index 0000000..d3c000e
--- /dev/null
@@ -0,0 +1,35 @@
+package org.argeo.cms.e4.users.providers;
+
+import org.argeo.api.cms.CmsContext;
+import org.argeo.api.acr.ldap.LdapAttr;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.cms.auth.UserAdminUtils;
+import org.argeo.cms.e4.users.SecurityAdminImages;
+import org.eclipse.swt.graphics.Image;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/** Provide a bundle specific image depending on the current user type */
+public class RoleIconLP extends UserAdminAbstractLP {
+       private static final long serialVersionUID = 6550449442061090388L;
+
+       @Override
+       public String getText(User user) {
+               return "";
+       }
+
+       @Override
+       public Image getImage(Object element) {
+               User user = (User) element;
+               String dn = user.getName();
+               if (dn.endsWith(CmsConstants.SYSTEM_ROLES_BASEDN))
+                       return SecurityAdminImages.ICON_ROLE;
+               else if (user.getType() == Role.GROUP) {
+                       String businessCategory = UserAdminUtils.getProperty(user, LdapAttr.businessCategory);
+                       if (businessCategory != null && businessCategory.equals(CmsContext.WORKGROUP))
+                               return SecurityAdminImages.ICON_WORKGROUP;
+                       return SecurityAdminImages.ICON_GROUP;
+               } else
+                       return SecurityAdminImages.ICON_USER;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserAdminAbstractLP.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserAdminAbstractLP.java
new file mode 100644 (file)
index 0000000..29873db
--- /dev/null
@@ -0,0 +1,66 @@
+package org.argeo.cms.e4.users.providers;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.cms.auth.UserAdminUtils;
+import org.argeo.cms.swt.CmsException;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.widgets.Display;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Utility class that add font modifications to a column label provider
+ * depending on the given user properties
+ */
+public abstract class UserAdminAbstractLP extends ColumnLabelProvider {
+       private static final long serialVersionUID = 137336765024922368L;
+
+       // private Font italic;
+       private Font bold;
+
+       @Override
+       public Font getFont(Object element) {
+               // Self as bold
+               try {
+                       LdapName selfUserName = UserAdminUtils.getCurrentUserLdapName();
+                       String userName = ((User) element).getName();
+                       LdapName userLdapName = new LdapName(userName);
+                       if (userLdapName.equals(selfUserName)) {
+                               if (bold == null)
+                                       bold = JFaceResources.getFontRegistry()
+                                                       .defaultFontDescriptor().setStyle(SWT.BOLD)
+                                                       .createFont(Display.getCurrent());
+                               return bold;
+                       }
+               } catch (InvalidNameException e) {
+                       throw new CmsException("cannot parse dn for " + element, e);
+               }
+
+               // Disabled as Italic
+               // Node userProfile = (Node) elem;
+               // if (!userProfile.getProperty(ARGEO_ENABLED).getBoolean())
+               // return italic;
+
+               return null;
+               // return super.getFont(element);
+       }
+
+       @Override
+       public String getText(Object element) {
+               User user = (User) element;
+               return getText(user);
+       }
+
+       public void setDisplay(Display display) {
+               // italic = JFaceResources.getFontRegistry().defaultFontDescriptor()
+               // .setStyle(SWT.ITALIC).createFont(display);
+               bold = JFaceResources.getFontRegistry().defaultFontDescriptor()
+                               .setStyle(SWT.BOLD).createFont(Display.getCurrent());
+       }
+
+       public abstract String getText(User user);
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserDragListener.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserDragListener.java
new file mode 100644 (file)
index 0000000..56a2624
--- /dev/null
@@ -0,0 +1,40 @@
+package org.argeo.cms.e4.users.providers;
+
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.osgi.service.useradmin.User;
+
+/** Default drag listener to modify group and users via the UI */
+public class UserDragListener implements DragSourceListener {
+       private static final long serialVersionUID = -2074337775033781454L;
+       private final Viewer viewer;
+
+       public UserDragListener(Viewer viewer) {
+               this.viewer = viewer;
+       }
+
+       public void dragStart(DragSourceEvent event) {
+               // TODO implement finer checks
+               IStructuredSelection selection = (IStructuredSelection) viewer
+                               .getSelection();
+               if (selection.isEmpty() || selection.size() > 1)
+                       event.doit = false;
+               else
+                       event.doit = true;
+       }
+
+       public void dragSetData(DragSourceEvent event) {
+               // TODO Support multiple selection
+               Object obj = ((IStructuredSelection) viewer.getSelection())
+                               .getFirstElement();
+               if (obj != null) {
+                       User user = (User) obj;
+                       event.data = user.getName();
+               }
+       }
+
+       public void dragFinished(DragSourceEvent event) {
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserFilter.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserFilter.java
new file mode 100644 (file)
index 0000000..2cfc10b
--- /dev/null
@@ -0,0 +1,58 @@
+package org.argeo.cms.e4.users.providers;
+
+import static org.argeo.eclipse.ui.EclipseUiUtils.notEmpty;
+
+import org.argeo.api.acr.ldap.LdapAttr;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.cms.auth.UserAdminUtils;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerFilter;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Filter user list using JFace mechanism on the client (yet on the server) side
+ * rather than having the UserAdmin to process the search
+ */
+public class UserFilter extends ViewerFilter {
+       private static final long serialVersionUID = 5082509381672880568L;
+
+       private String searchString;
+       private boolean showSystemRole = true;
+
+       private final String[] knownProps = { LdapAttr.DN, LdapAttr.cn.name(), LdapAttr.givenName.name(),
+                       LdapAttr.sn.name(), LdapAttr.uid.name(), LdapAttr.description.name(), LdapAttr.mail.name() };
+
+       public void setSearchText(String s) {
+               // ensure that the value can be used for matching
+               if (notEmpty(s))
+                       searchString = ".*" + s.toLowerCase() + ".*";
+               else
+                       searchString = ".*";
+       }
+
+       public void setShowSystemRole(boolean showSystemRole) {
+               this.showSystemRole = showSystemRole;
+       }
+
+       @Override
+       public boolean select(Viewer viewer, Object parentElement, Object element) {
+               User user = (User) element;
+               if (!showSystemRole && user.getName().matches(".*(" + CmsConstants.SYSTEM_ROLES_BASEDN + ")"))
+                       // UserAdminUtils.getProperty(user, LdifName.dn.name())
+                       // .toLowerCase().endsWith(AuthConstants.ROLES_BASEDN))
+                       return false;
+
+               if (searchString == null || searchString.length() == 0)
+                       return true;
+
+               if (user.getName().matches(searchString))
+                       return true;
+
+               for (String key : knownProps) {
+                       String currVal = UserAdminUtils.getProperty(user, key);
+                       if (notEmpty(currVal) && currVal.toLowerCase().matches(searchString))
+                               return true;
+               }
+               return false;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserNameLP.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserNameLP.java
new file mode 100644 (file)
index 0000000..3cd00eb
--- /dev/null
@@ -0,0 +1,13 @@
+package org.argeo.cms.e4.users.providers;
+
+import org.osgi.service.useradmin.User;
+
+/** Simply declare a label provider that returns the username of a user */
+public class UserNameLP extends UserAdminAbstractLP {
+       private static final long serialVersionUID = 6550449442061090388L;
+
+       @Override
+       public String getText(User user) {
+               return user.getName();
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/package-info.java
new file mode 100644 (file)
index 0000000..33bef8d
--- /dev/null
@@ -0,0 +1,2 @@
+/** Users management content providers. */
+package org.argeo.cms.e4.users.providers;
\ No newline at end of file
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/jcr/e4/rap/CmsE4AdminApp.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/jcr/e4/rap/CmsE4AdminApp.java
new file mode 100644 (file)
index 0000000..3af4472
--- /dev/null
@@ -0,0 +1,17 @@
+package org.argeo.cms.jcr.e4.rap;
+
+import org.argeo.cms.e4.rap.AbstractRapE4App;
+import org.eclipse.rap.rwt.application.Application;
+
+/**
+ * Access to canonical views of the core CMS concepts, useful for devleopers and
+ * operators.
+ */
+public class CmsE4AdminApp extends AbstractRapE4App {
+       @Override
+       protected void addEntryPoints(Application application) {
+               addE4EntryPoint(application, "/devops", "org.argeo.tool.devops.e4/e4xmi/devops.e4xmi",
+                               customise("Argeo Tool DevOps"));
+       }
+
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/LdifUsersTable.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/LdifUsersTable.java
new file mode 100644 (file)
index 0000000..a30d2f7
--- /dev/null
@@ -0,0 +1,401 @@
+package org.argeo.cms.swt.useradmin;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.jface.layout.TableColumnLayout;
+import org.eclipse.jface.viewers.CheckboxTableViewer;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ColumnWeightData;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.Text;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Generic composite that display a filter and a table viewer to display users
+ * (can also be groups)
+ * 
+ * Warning: this class does not extends <code>TableViewer</code>. Use the
+ * getTableViewer method to access it.
+ * 
+ */
+public abstract class LdifUsersTable extends Composite {
+       private static final long serialVersionUID = -7385959046279360420L;
+
+       // Context
+       // private UserAdmin userAdmin;
+
+       // Configuration
+       private List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+       private boolean hasFilter;
+       private boolean preventTableLayout = false;
+       private boolean hasSelectionColumn;
+       private int tableStyle;
+
+       // Local UI Objects
+       private TableViewer usersViewer;
+       private Text filterTxt;
+
+       /* EXPOSED METHODS */
+
+       /**
+        * @param parent
+        * @param style
+        */
+       public LdifUsersTable(Composite parent, int style) {
+               super(parent, SWT.NO_FOCUS);
+               this.tableStyle = style;
+       }
+
+       // TODO workaround the bug of the table layout in the Form
+       public LdifUsersTable(Composite parent, int style, boolean preventTableLayout) {
+               super(parent, SWT.NO_FOCUS);
+               this.tableStyle = style;
+               this.preventTableLayout = preventTableLayout;
+       }
+
+       /** This must be called before the call to populate method */
+       public void setColumnDefinitions(List<ColumnDefinition> columnDefinitions) {
+               this.columnDefs = columnDefinitions;
+       }
+
+       /**
+        * 
+        * @param addFilter
+        *            choose to add a field to filter results or not
+        * @param addSelection
+        *            choose to add a column to select some of the displayed results or
+        *            not
+        */
+       public void populate(boolean addFilter, boolean addSelection) {
+               // initialization
+               Composite parent = this;
+               hasFilter = addFilter;
+               hasSelectionColumn = addSelection;
+
+               // Main Layout
+               GridLayout layout = EclipseUiUtils.noSpaceGridLayout();
+               layout.verticalSpacing = 5;
+               this.setLayout(layout);
+               if (hasFilter)
+                       createFilterPart(parent);
+
+               Composite tableComp = new Composite(parent, SWT.NO_FOCUS);
+               tableComp.setLayoutData(EclipseUiUtils.fillAll());
+               usersViewer = createTableViewer(tableComp);
+               usersViewer.setContentProvider(new UsersContentProvider());
+       }
+
+       /**
+        * 
+        * @param showMore
+        *            display static filters on creation
+        * @param addSelection
+        *            choose to add a column to select some of the displayed results or
+        *            not
+        */
+       public void populateWithStaticFilters(boolean showMore, boolean addSelection) {
+               // initialization
+               Composite parent = this;
+               hasFilter = true;
+               hasSelectionColumn = addSelection;
+
+               // Main Layout
+               GridLayout layout = EclipseUiUtils.noSpaceGridLayout();
+               layout.verticalSpacing = 5;
+               this.setLayout(layout);
+               createStaticFilterPart(parent, showMore);
+
+               Composite tableComp = new Composite(parent, SWT.NO_FOCUS);
+               tableComp.setLayoutData(EclipseUiUtils.fillAll());
+               usersViewer = createTableViewer(tableComp);
+               usersViewer.setContentProvider(new UsersContentProvider());
+       }
+
+       /** Enable access to the selected users or groups */
+       public List<User> getSelectedUsers() {
+               if (hasSelectionColumn) {
+                       Object[] elements = ((CheckboxTableViewer) usersViewer).getCheckedElements();
+
+                       List<User> result = new ArrayList<User>();
+                       for (Object obj : elements) {
+                               result.add((User) obj);
+                       }
+                       return result;
+               } else
+                       throw new EclipseUiException(
+                                       "Unvalid request: no selection column " + "has been created for the current table");
+       }
+
+       /** Returns the User table viewer, typically to add doubleclick listener */
+       public TableViewer getTableViewer() {
+               return usersViewer;
+       }
+
+       /**
+        * Force the refresh of the underlying table using the current filter string if
+        * relevant
+        */
+       public void refresh() {
+               String filter = hasFilter ? filterTxt.getText().trim() : null;
+               if ("".equals(filter))
+                       filter = null;
+               refreshFilteredList(filter);
+       }
+
+       /** Effective repository request: caller must implement this method */
+       abstract protected List<User> listFilteredElements(String filter);
+
+       // protected List<User> listFilteredElements(String filter) {
+       // List<User> users = new ArrayList<User>();
+       // try {
+       // Role[] roles = userAdmin.getRoles(filter);
+       // // Display all users and groups
+       // for (Role role : roles)
+       // users.add((User) role);
+       // } catch (InvalidSyntaxException e) {
+       // throw new EclipseUiException("Unable to get roles with filter: "
+       // + filter, e);
+       // }
+       // return users;
+       // }
+
+       /* GENERIC COMPOSITE METHODS */
+       @Override
+       public boolean setFocus() {
+               if (hasFilter)
+                       return filterTxt.setFocus();
+               else
+                       return usersViewer.getTable().setFocus();
+       }
+
+       @Override
+       public void dispose() {
+               super.dispose();
+       }
+
+       /* LOCAL CLASSES AND METHODS */
+       // Will be usefull to rather use a virtual table viewer
+       private void refreshFilteredList(String filter) {
+               List<User> users = listFilteredElements(filter);
+               usersViewer.setInput(users.toArray());
+       }
+
+       private class UsersContentProvider implements IStructuredContentProvider {
+               private static final long serialVersionUID = 1L;
+
+               public Object[] getElements(Object inputElement) {
+                       return (Object[]) inputElement;
+               }
+
+               public void dispose() {
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+       }
+
+       /* MANAGE FILTER */
+       private void createFilterPart(Composite parent) {
+               // Text Area for the filter
+               filterTxt = new Text(parent, SWT.BORDER | SWT.SEARCH | SWT.ICON_SEARCH | SWT.ICON_CANCEL);
+               filterTxt.setLayoutData(new GridData(GridData.FILL, GridData.FILL, true, false));
+               filterTxt.addModifyListener(new ModifyListener() {
+                       private static final long serialVersionUID = 1L;
+
+                       public void modifyText(ModifyEvent event) {
+                               refreshFilteredList(filterTxt.getText());
+                       }
+               });
+       }
+
+       private void createStaticFilterPart(Composite parent, boolean showMore) {
+               Composite filterComp = new Composite(parent, SWT.NO_FOCUS);
+               filterComp.setLayout(new GridLayout(2, false));
+               filterComp.setLayoutData(EclipseUiUtils.fillWidth());
+               // generic search
+               filterTxt = new Text(filterComp, SWT.BORDER | SWT.SEARCH | SWT.ICON_SEARCH | SWT.ICON_CANCEL);
+               filterTxt.setLayoutData(new GridData(GridData.FILL, GridData.FILL, true, false));
+               // filterTxt.setLayoutData(new GridData(GridData.GRAB_HORIZONTAL |
+               // GridData.HORIZONTAL_ALIGN_FILL));
+               filterTxt.addModifyListener(new ModifyListener() {
+                       private static final long serialVersionUID = 1L;
+
+                       public void modifyText(ModifyEvent event) {
+                               refreshFilteredList(filterTxt.getText());
+                       }
+               });
+
+               // add static filter abilities
+               Link moreLk = new Link(filterComp, SWT.NONE);
+               Composite staticFilterCmp = new Composite(filterComp, SWT.NO_FOCUS);
+               staticFilterCmp.setLayoutData(EclipseUiUtils.fillWidth(2));
+               populateStaticFilters(staticFilterCmp);
+
+               MoreLinkListener listener = new MoreLinkListener(moreLk, staticFilterCmp, showMore);
+               // initialise the layout
+               listener.refresh();
+               moreLk.addSelectionListener(listener);
+       }
+
+       /** Overwrite to add static filters */
+       protected void populateStaticFilters(Composite staticFilterCmp) {
+       }
+
+       // private void addMoreSL(final Link more) {
+       // more.addSelectionListener( }
+
+       private class MoreLinkListener extends SelectionAdapter {
+               private static final long serialVersionUID = -524987616510893463L;
+               private boolean isShown;
+               private final Composite staticFilterCmp;
+               private final Link moreLk;
+
+               public MoreLinkListener(Link moreLk, Composite staticFilterCmp, boolean isShown) {
+                       this.moreLk = moreLk;
+                       this.staticFilterCmp = staticFilterCmp;
+                       this.isShown = isShown;
+               }
+
+               @Override
+               public void widgetSelected(SelectionEvent e) {
+                       isShown = !isShown;
+                       refresh();
+               }
+
+               public void refresh() {
+                       GridData gd = (GridData) staticFilterCmp.getLayoutData();
+                       if (isShown) {
+                               moreLk.setText("<a> Less... </a>");
+                               gd.heightHint = SWT.DEFAULT;
+                       } else {
+                               moreLk.setText("<a> More... </a>");
+                               gd.heightHint = 0;
+                       }
+                       forceLayout();
+               }
+       }
+
+       private void forceLayout() {
+               LdifUsersTable.this.getParent().layout(true, true);
+       }
+
+       private TableViewer createTableViewer(final Composite parent) {
+
+               int style = tableStyle | SWT.H_SCROLL | SWT.V_SCROLL;
+               if (hasSelectionColumn)
+                       style = style | SWT.CHECK;
+               Table table = new Table(parent, style);
+               TableColumnLayout layout = new TableColumnLayout();
+
+               // TODO the table layout does not works with the scrolled form
+
+               if (preventTableLayout) {
+                       parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+                       table.setLayoutData(EclipseUiUtils.fillAll());
+               } else
+                       parent.setLayout(layout);
+
+               TableViewer viewer;
+               if (hasSelectionColumn)
+                       viewer = new CheckboxTableViewer(table);
+               else
+                       viewer = new TableViewer(table);
+               table.setLinesVisible(true);
+               table.setHeaderVisible(true);
+
+               TableViewerColumn column;
+               // int offset = 0;
+               if (hasSelectionColumn) {
+                       // offset = 1;
+                       column = ViewerUtils.createTableViewerColumn(viewer, "", SWT.NONE, 25);
+                       column.setLabelProvider(new ColumnLabelProvider() {
+                               private static final long serialVersionUID = 1L;
+
+                               @Override
+                               public String getText(Object element) {
+                                       return null;
+                               }
+                       });
+                       layout.setColumnData(column.getColumn(), new ColumnWeightData(25, 25, false));
+
+                       SelectionAdapter selectionAdapter = new SelectionAdapter() {
+                               private static final long serialVersionUID = 1L;
+
+                               boolean allSelected = false;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       allSelected = !allSelected;
+                                       ((CheckboxTableViewer) usersViewer).setAllChecked(allSelected);
+                               }
+                       };
+                       column.getColumn().addSelectionListener(selectionAdapter);
+               }
+
+               // NodeViewerComparator comparator = new NodeViewerComparator();
+               // TODO enable the sort by click on the header
+               // int i = offset;
+               for (ColumnDefinition colDef : columnDefs)
+                       createTableColumn(viewer, layout, colDef);
+
+               // column = ViewerUtils.createTableViewerColumn(viewer,
+               // colDef.getHeaderLabel(), SWT.NONE, colDef.getColumnSize());
+               // column.setLabelProvider(new CLProvider(colDef.getPropertyName()));
+               // column.getColumn().addSelectionListener(
+               // JcrUiUtils.getNodeSelectionAdapter(i,
+               // colDef.getPropertyType(), colDef.getPropertyName(),
+               // comparator, viewer));
+               // i++;
+               // }
+
+               // IMPORTANT: initialize comparator before setting it
+               // JcrColumnDefinition firstCol = colDefs.get(0);
+               // comparator.setColumn(firstCol.getPropertyType(),
+               // firstCol.getPropertyName());
+               // viewer.setComparator(comparator);
+
+               return viewer;
+       }
+
+       /** Default creation of a column for a user table */
+       private TableViewerColumn createTableColumn(TableViewer tableViewer, TableColumnLayout layout,
+                       ColumnDefinition columnDef) {
+
+               boolean resizable = true;
+               TableViewerColumn tvc = new TableViewerColumn(tableViewer, SWT.NONE);
+               TableColumn column = tvc.getColumn();
+
+               column.setText(columnDef.getLabel());
+               column.setWidth(columnDef.getMinWidth());
+               column.setResizable(resizable);
+
+               ColumnLabelProvider lp = columnDef.getLabelProvider();
+               // add a reference to the display to enable font management
+               // if (lp instanceof UserAdminAbstractLP)
+               // ((UserAdminAbstractLP) lp).setDisplay(tableViewer.getTable()
+               // .getDisplay());
+               tvc.setLabelProvider(lp);
+
+               layout.setColumnData(column, new ColumnWeightData(columnDef.getWeight(), columnDef.getMinWidth(), resizable));
+
+               return tvc;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/PickUpUserDialog.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/PickUpUserDialog.java
new file mode 100644 (file)
index 0000000..3e70ca3
--- /dev/null
@@ -0,0 +1,245 @@
+package org.argeo.cms.swt.useradmin;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.argeo.api.acr.ldap.LdapAttr;
+import org.argeo.api.acr.ldap.LdapObj;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.TrayDialog;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+
+/** Dialog with a user (or group) list to pick up one */
+public class PickUpUserDialog extends TrayDialog {
+       private static final long serialVersionUID = -1420106871173920369L;
+
+       // Business objects
+       private final UserAdmin userAdmin;
+       private User selectedUser;
+
+       // this page widgets and UI objects
+       private String title;
+       private LdifUsersTable userTableViewerCmp;
+       private TableViewer userViewer;
+       private List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+
+       /**
+        * A dialog to pick up a group or a user, showing a table with default
+        * columns
+        */
+       public PickUpUserDialog(Shell parentShell, String title, UserAdmin userAdmin) {
+               super(parentShell);
+               this.title = title;
+               this.userAdmin = userAdmin;
+
+               columnDefs.add(new ColumnDefinition(new UserLP(UserLP.COL_ICON), "",
+                               24, 24));
+               columnDefs.add(new ColumnDefinition(
+                               new UserLP(UserLP.COL_DISPLAY_NAME), "Common Name", 150, 100));
+               columnDefs.add(new ColumnDefinition(new UserLP(UserLP.COL_DOMAIN),
+                               "Domain", 100, 120));
+               columnDefs.add(new ColumnDefinition(new UserLP(UserLP.COL_DN),
+                               "Distinguished Name", 300, 100));
+       }
+
+       /** A dialog to pick up a group or a user */
+       public PickUpUserDialog(Shell parentShell, String title,
+                       UserAdmin userAdmin, List<ColumnDefinition> columnDefs) {
+               super(parentShell);
+               this.title = title;
+               this.userAdmin = userAdmin;
+               this.columnDefs = columnDefs;
+       }
+
+       @Override
+       protected void okPressed() {
+               if (getSelected() == null)
+                       MessageDialog.openError(getShell(), "No user chosen",
+                                       "Please, choose a user or press Cancel.");
+               else
+                       super.okPressed();
+       }
+
+       protected Control createDialogArea(Composite parent) {
+               Composite dialogArea = (Composite) super.createDialogArea(parent);
+               dialogArea.setLayout(new FillLayout());
+
+               Composite bodyCmp = new Composite(dialogArea, SWT.NO_FOCUS);
+               bodyCmp.setLayout(new GridLayout());
+
+               // Create and configure the table
+               userTableViewerCmp = new MyUserTableViewer(bodyCmp, SWT.MULTI
+                               | SWT.H_SCROLL | SWT.V_SCROLL);
+
+               userTableViewerCmp.setColumnDefinitions(columnDefs);
+               userTableViewerCmp.populateWithStaticFilters(false, false);
+               GridData gd = EclipseUiUtils.fillAll();
+               gd.minimumHeight = 300;
+               userTableViewerCmp.setLayoutData(gd);
+               userTableViewerCmp.refresh();
+
+               // Controllers
+               userViewer = userTableViewerCmp.getTableViewer();
+               userViewer.addDoubleClickListener(new MyDoubleClickListener());
+               userViewer
+                               .addSelectionChangedListener(new MySelectionChangedListener());
+
+               parent.pack();
+               return dialogArea;
+       }
+
+       public User getSelected() {
+               if (selectedUser == null)
+                       return null;
+               else
+                       return selectedUser;
+       }
+
+       protected void configureShell(Shell shell) {
+               super.configureShell(shell);
+               shell.setText(title);
+       }
+
+       class MyDoubleClickListener implements IDoubleClickListener {
+               public void doubleClick(DoubleClickEvent evt) {
+                       if (evt.getSelection().isEmpty())
+                               return;
+
+                       Object obj = ((IStructuredSelection) evt.getSelection())
+                                       .getFirstElement();
+                       if (obj instanceof User) {
+                               selectedUser = (User) obj;
+                               okPressed();
+                       }
+               }
+       }
+
+       class MySelectionChangedListener implements ISelectionChangedListener {
+               @Override
+               public void selectionChanged(SelectionChangedEvent event) {
+                       if (event.getSelection().isEmpty()) {
+                               selectedUser = null;
+                               return;
+                       }
+                       Object obj = ((IStructuredSelection) event.getSelection())
+                                       .getFirstElement();
+                       if (obj instanceof Group) {
+                               selectedUser = (Group) obj;
+                       }
+               }
+       }
+
+       private class MyUserTableViewer extends LdifUsersTable {
+               private static final long serialVersionUID = 8467999509931900367L;
+
+               private final String[] knownProps = { LdapAttr.uid.name(),
+                               LdapAttr.cn.name(), LdapAttr.DN };
+
+               private Button showSystemRoleBtn;
+               private Button showUserBtn;
+
+               public MyUserTableViewer(Composite parent, int style) {
+                       super(parent, style);
+               }
+
+               protected void populateStaticFilters(Composite staticFilterCmp) {
+                       staticFilterCmp.setLayout(new GridLayout());
+                       showSystemRoleBtn = new Button(staticFilterCmp, SWT.CHECK);
+                       showSystemRoleBtn.setText("Show system roles  ");
+
+                       showUserBtn = new Button(staticFilterCmp, SWT.CHECK);
+                       showUserBtn.setText("Show users  ");
+
+                       SelectionListener sl = new SelectionAdapter() {
+                               private static final long serialVersionUID = -7033424592697691676L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       refresh();
+                               }
+                       };
+
+                       showSystemRoleBtn.addSelectionListener(sl);
+                       showUserBtn.addSelectionListener(sl);
+               }
+
+               @Override
+               protected List<User> listFilteredElements(String filter) {
+                       Role[] roles;
+                       try {
+                               StringBuilder builder = new StringBuilder();
+
+                               StringBuilder filterBuilder = new StringBuilder();
+                               if (notNull(filter))
+                                       for (String prop : knownProps) {
+                                               filterBuilder.append("(");
+                                               filterBuilder.append(prop);
+                                               filterBuilder.append("=*");
+                                               filterBuilder.append(filter);
+                                               filterBuilder.append("*)");
+                                       }
+
+                               String typeStr = "(" + LdapAttr.objectClass.name() + "="
+                                               + LdapObj.groupOfNames.name() + ")";
+                               if ((showUserBtn.getSelection()))
+                                       typeStr = "(|(" + LdapAttr.objectClass.name() + "="
+                                                       + LdapObj.inetOrgPerson.name() + ")" + typeStr
+                                                       + ")";
+
+                               if (!showSystemRoleBtn.getSelection())
+                                       typeStr = "(& " + typeStr + "(!(" + LdapAttr.DN + "=*"
+                                                       + CmsConstants.SYSTEM_ROLES_BASEDN + ")))";
+
+                               if (filterBuilder.length() > 1) {
+                                       builder.append("(&" + typeStr);
+                                       builder.append("(|");
+                                       builder.append(filterBuilder.toString());
+                                       builder.append("))");
+                               } else {
+                                       builder.append(typeStr);
+                               }
+                               roles = userAdmin.getRoles(builder.toString());
+                       } catch (InvalidSyntaxException e) {
+                               throw new EclipseUiException(
+                                               "Unable to get roles with filter: " + filter, e);
+                       }
+                       List<User> users = new ArrayList<User>();
+                       for (Role role : roles)
+                               if (!users.contains(role))
+                                       users.add((User) role);
+                       return users;
+               }
+       }
+
+       private boolean notNull(String string) {
+               if (string == null)
+                       return false;
+               else
+                       return !"".equals(string.trim());
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/UserLP.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/UserLP.java
new file mode 100644 (file)
index 0000000..b3ab40e
--- /dev/null
@@ -0,0 +1,76 @@
+package org.argeo.cms.swt.useradmin;
+
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.cms.auth.UserAdminUtils;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/** Centralize label providers for the group table */
+class UserLP extends ColumnLabelProvider {
+       private static final long serialVersionUID = -4645930210988368571L;
+
+       final static String COL_ICON = "colID.icon";
+       final static String COL_DN = "colID.dn";
+       final static String COL_DISPLAY_NAME = "colID.displayName";
+       final static String COL_DOMAIN = "colID.domain";
+
+       final String currType;
+
+       // private Font italic;
+       private Font bold;
+
+       UserLP(String colId) {
+               this.currType = colId;
+       }
+
+       @Override
+       public Font getFont(Object element) {
+               // Current user as bold
+               if (UserAdminUtils.isCurrentUser(((User) element))) {
+                       if (bold == null)
+                               bold = JFaceResources.getFontRegistry().defaultFontDescriptor().setStyle(SWT.BOLD)
+                                               .createFont(Display.getCurrent());
+                       return bold;
+               }
+               return null;
+       }
+
+       @Override
+       public Image getImage(Object element) {
+               if (COL_ICON.equals(currType)) {
+                       User user = (User) element;
+                       String dn = user.getName();
+                       if (dn.endsWith(CmsConstants.SYSTEM_ROLES_BASEDN))
+                               return UsersImages.ICON_ROLE;
+                       else if (user.getType() == Role.GROUP)
+                               return UsersImages.ICON_GROUP;
+                       else
+                               return UsersImages.ICON_USER;
+               } else
+                       return null;
+       }
+
+       @Override
+       public String getText(Object element) {
+               User user = (User) element;
+               return getText(user);
+
+       }
+
+       public String getText(User user) {
+               if (COL_DN.equals(currType))
+                       return user.getName();
+               else if (COL_DISPLAY_NAME.equals(currType))
+                       return UserAdminUtils.getCommonName(user);
+               else if (COL_DOMAIN.equals(currType))
+                       return UserAdminUtils.getDomainName(user);
+               else
+                       return "";
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/UsersImages.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/UsersImages.java
new file mode 100644 (file)
index 0000000..21fc5af
--- /dev/null
@@ -0,0 +1,14 @@
+package org.argeo.cms.swt.useradmin;
+
+import org.argeo.cms.ui.theme.CmsImages;
+import org.eclipse.swt.graphics.Image;
+
+/** Specific users icons. */
+public class UsersImages {
+       private final static String PREFIX = "icons/";
+
+       public final static Image ICON_USER = CmsImages.createImg(PREFIX + "person.png");
+       public final static Image ICON_GROUP = CmsImages.createImg(PREFIX + "group.png");
+       public final static Image ICON_ROLE = CmsImages.createImg(PREFIX + "role.gif");
+       public final static Image ICON_CHANGE_PASSWORD = CmsImages.createImg(PREFIX + "security.gif");
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/ViewerUtils.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/ViewerUtils.java
new file mode 100644 (file)
index 0000000..d186783
--- /dev/null
@@ -0,0 +1,58 @@
+package org.argeo.cms.swt.useradmin;
+
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.TreeViewerColumn;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TreeColumn;
+
+/**
+ * Centralise useful methods to manage JFace Table, Tree and TreeColumn viewers.
+ */
+public class ViewerUtils {
+
+       /**
+        * Creates a basic column for the given table. For the time being, we do not
+        * support movable columns.
+        */
+       public static TableColumn createColumn(Table parent, String name, int style, int width) {
+               TableColumn result = new TableColumn(parent, style);
+               result.setText(name);
+               result.setWidth(width);
+               result.setResizable(true);
+               return result;
+       }
+
+       /**
+        * Creates a TableViewerColumn for the given viewer. For the time being, we do
+        * not support movable columns.
+        */
+       public static TableViewerColumn createTableViewerColumn(TableViewer parent, String name, int style, int width) {
+               TableViewerColumn tvc = new TableViewerColumn(parent, style);
+               TableColumn column = tvc.getColumn();
+               column.setText(name);
+               column.setWidth(width);
+               column.setResizable(true);
+               return tvc;
+       }
+
+       // public static TableViewerColumn createTableViewerColumn(TableViewer parent,
+       // Localized name, int style, int width) {
+       // return createTableViewerColumn(parent, name.lead(), style, width);
+       // }
+
+       /**
+        * Creates a TreeViewerColumn for the given viewer. For the time being, we do
+        * not support movable columns.
+        */
+       public static TreeViewerColumn createTreeViewerColumn(TreeViewer parent, String name, int style, int width) {
+               TreeViewerColumn tvc = new TreeViewerColumn(parent, style);
+               TreeColumn column = tvc.getColumn();
+               column.setText(name);
+               column.setWidth(width);
+               column.setResizable(true);
+               return tvc;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/package-info.java
new file mode 100644 (file)
index 0000000..3597bfc
--- /dev/null
@@ -0,0 +1,2 @@
+/** SWT/JFace users management components. */
+package org.argeo.cms.swt.useradmin;
\ No newline at end of file
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/AbstractFormPart.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/AbstractFormPart.java
new file mode 100644 (file)
index 0000000..4ce4688
--- /dev/null
@@ -0,0 +1,108 @@
+package org.argeo.cms.ui.eclipse.forms;
+/**
+ * AbstractFormPart implements IFormPart interface and can be used as a
+ * convenient base class for concrete form parts. If a method contains
+ * code that must be called, look for instructions to call 'super'
+ * when overriding.
+ * 
+ * @see org.eclipse.ui.forms.widgets.Section
+ * @since 1.0
+ */
+public abstract class AbstractFormPart implements IFormPart {
+       private IManagedForm managedForm;
+       private boolean dirty = false;
+       private boolean stale = true;
+       /**
+        * @see org.eclipse.ui.forms.IFormPart#initialize(org.eclipse.ui.forms.IManagedForm)
+        */
+       public void initialize(IManagedForm form) {
+               this.managedForm = form;
+       }
+       /**
+        * Returns the form that manages this part.
+        * 
+        * @return the managed form
+        */
+       public IManagedForm getManagedForm() {
+               return managedForm;
+       }
+       /**
+        * Disposes the part. Subclasses should override to release any system
+        * resources.
+        */
+       public void dispose() {
+       }
+       /**
+        * Commits the part. Subclasses should call 'super' when overriding.
+        * 
+        * @param onSave
+        *            <code>true</code> if the request to commit has arrived as a
+        *            result of the 'save' action.
+        */
+       public void commit(boolean onSave) {
+               dirty = false;
+       }
+       /**
+        * Sets the overall form input. Subclases may elect to override the method
+        * and adjust according to the form input.
+        * 
+        * @param input
+        *            the form input object
+        * @return <code>false</code>
+        */
+       public boolean setFormInput(Object input) {
+               return false;
+       }
+       /**
+        * Instructs the part to grab keyboard focus.
+        */
+       public void setFocus() {
+       }
+       /**
+        * Refreshes the section after becoming stale (falling behind data in the
+        * model). Subclasses must call 'super' when overriding this method.
+        */
+       public void refresh() {
+               stale = false;
+               // since we have refreshed, any changes we had in the
+               // part are gone and we are not dirty
+               dirty = false;
+       }
+       /**
+        * Marks the part dirty. Subclasses should call this method as a result of
+        * user interaction with the widgets in the section.
+        */
+       public void markDirty() {
+               dirty = true;
+               managedForm.dirtyStateChanged();
+       }
+       /**
+        * Tests whether the part is dirty i.e. its widgets have state that is
+        * newer than the data in the model.
+        * 
+        * @return <code>true</code> if the part is dirty, <code>false</code>
+        *         otherwise.
+        */
+       public boolean isDirty() {
+               return dirty;
+       }
+       /**
+        * Tests whether the part is stale i.e. its widgets have state that is
+        * older than the data in the model.
+        * 
+        * @return <code>true</code> if the part is stale, <code>false</code>
+        *         otherwise.
+        */
+       public boolean isStale() {
+               return stale;
+       }
+       /**
+        * Marks the part stale. Subclasses should call this method as a result of
+        * model notification that indicates that the content of the section is no
+        * longer in sync with the model.
+        */
+       public void markStale() {
+               stale = true;
+               managedForm.staleStateChanged();
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormColors.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormColors.java
new file mode 100644 (file)
index 0000000..32b031b
--- /dev/null
@@ -0,0 +1,730 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.resource.LocalResourceManager;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.RGB;
+//import org.eclipse.swt.internal.graphics.Graphics;
+import org.eclipse.swt.widgets.Display;
+
+/**
+ * Manages colors that will be applied to forms and form widgets. The colors are
+ * chosen to make the widgets look correct in the editor area. If a different
+ * set of colors is needed, subclass this class and override 'initialize' and/or
+ * 'initializeColors'.
+ * 
+ * @since 1.0
+ */
+public class FormColors {
+       /**
+        * Key for the form title foreground color.
+        * 
+        * @deprecated use <code>IFormColors.TITLE</code>.
+        */
+       public static final String TITLE = IFormColors.TITLE;
+
+       /**
+        * Key for the tree/table border color.
+        * 
+        * @deprecated use <code>IFormColors.BORDER</code>
+        */
+       public static final String BORDER = IFormColors.BORDER;
+
+       /**
+        * Key for the section separator color.
+        * 
+        * @deprecated use <code>IFormColors.SEPARATOR</code>.
+        */
+       public static final String SEPARATOR = IFormColors.SEPARATOR;
+
+       /**
+        * Key for the section title bar background.
+        * 
+        * @deprecated use <code>IFormColors.TB_BG
+        */
+       public static final String TB_BG = IFormColors.TB_BG;
+
+       /**
+        * Key for the section title bar foreground.
+        * 
+        * @deprecated use <code>IFormColors.TB_FG</code>
+        */
+       public static final String TB_FG = IFormColors.TB_FG;
+
+       /**
+        * Key for the section title bar gradient.
+        * 
+        * @deprecated use <code>IFormColors.TB_GBG</code>
+        */
+       public static final String TB_GBG = IFormColors.TB_GBG;
+
+       /**
+        * Key for the section title bar border.
+        * 
+        * @deprecated use <code>IFormColors.TB_BORDER</code>.
+        */
+       public static final String TB_BORDER = IFormColors.TB_BORDER;
+
+       /**
+        * Key for the section toggle color. Since 3.1, this color is used for all
+        * section styles.
+        * 
+        * @deprecated use <code>IFormColors.TB_TOGGLE</code>.
+        */
+       public static final String TB_TOGGLE = IFormColors.TB_TOGGLE;
+
+       /**
+        * Key for the section toggle hover color.
+        * 
+        * @deprecated use <code>IFormColors.TB_TOGGLE_HOVER</code>.
+        */
+       public static final String TB_TOGGLE_HOVER = IFormColors.TB_TOGGLE_HOVER;
+
+       protected Map colorRegistry = new HashMap(10);
+
+       private LocalResourceManager resources;
+
+       protected Color background;
+
+       protected Color foreground;
+
+       private boolean shared;
+
+       protected Display display;
+
+       protected Color border;
+
+       /**
+        * Creates form colors using the provided display.
+        * 
+        * @param display
+        *            the display to use
+        */
+       public FormColors(Display display) {
+               this.display = display;
+               initialize();
+       }
+
+       /**
+        * Returns the display used to create colors.
+        * 
+        * @return the display
+        */
+       public Display getDisplay() {
+               return display;
+       }
+
+       /**
+        * Initializes the colors. Subclasses can override this method to change the
+        * way colors are created. Alternatively, only the color table can be
+        * modified by overriding <code>initializeColorTable()</code>.
+        * 
+        * @see #initializeColorTable
+        */
+       protected void initialize() {
+               background = display.getSystemColor(SWT.COLOR_LIST_BACKGROUND);
+               foreground = display.getSystemColor(SWT.COLOR_LIST_FOREGROUND);
+               initializeColorTable();
+               updateBorderColor();
+       }
+
+       /**
+        * Allocates colors for the following keys: BORDER, SEPARATOR and
+        * TITLE. Subclasses can override to allocate these colors differently.
+        */
+       protected void initializeColorTable() {
+               createTitleColor();
+               createColor(IFormColors.SEPARATOR, getColor(IFormColors.TITLE).getRGB());
+               RGB black = getSystemColor(SWT.COLOR_BLACK);
+               RGB borderRGB = getSystemColor(SWT.COLOR_TITLE_INACTIVE_BACKGROUND_GRADIENT);
+               createColor(IFormColors.BORDER, blend(borderRGB, black, 80));
+       }
+
+       /**
+        * Allocates colors for the section tool bar (all the keys that start with
+        * TB). Since these colors are only needed when TITLE_BAR style is used with
+        * the Section widget, they are not needed all the time and are allocated on
+        * demand. Consequently, this method will do nothing if the colors have been
+        * already initialized. Call this method prior to using colors with the TB
+        * keys to ensure they are available.
+        */
+       public void initializeSectionToolBarColors() {
+               if (colorRegistry.containsKey(IFormColors.TB_BG))
+                       return;
+               createTitleBarGradientColors();
+               createTitleBarOutlineColors();
+               createTwistieColors();
+       }
+
+       /**
+        * Allocates additional colors for the form header, namely background
+        * gradients, bottom separator keylines and DND highlights. Since these
+        * colors are only needed for clients that want to use these particular
+        * style of header rendering, they are not needed all the time and are
+        * allocated on demand. Consequently, this method will do nothing if the
+        * colors have been already initialized. Call this method prior to using
+        * color keys with the H_ prefix to ensure they are available.
+        */
+       protected void initializeFormHeaderColors() {
+               if (colorRegistry.containsKey(IFormColors.H_BOTTOM_KEYLINE2))
+                       return;
+               createFormHeaderColors();
+       }
+
+       /**
+        * Returns the RGB value of the system color represented by the code
+        * argument, as defined in <code>SWT</code> class.
+        * 
+        * @param code
+        *            the system color constant as defined in <code>SWT</code>
+        *            class.
+        * @return the RGB value of the system color
+        */
+       public RGB getSystemColor(int code) {
+               return getDisplay().getSystemColor(code).getRGB();
+       }
+
+       /**
+        * Creates the color for the specified key using the provided RGB object.
+        * The color object will be returned and also put into the registry. When
+        * the class is disposed, the color will be disposed with it.
+        * 
+        * @param key
+        *            the unique color key
+        * @param rgb
+        *            the RGB object
+        * @return the allocated color object
+        */
+       public Color createColor(String key, RGB rgb) {
+               // RAP [rh] changes due to missing Color constructor
+//             Color c = getResourceManager().createColor(rgb);
+//             Color prevC = (Color) colorRegistry.get(key);
+//             if (prevC != null && !prevC.isDisposed())
+//                     getResourceManager().destroyColor(prevC.getRGB());
+//             Color c = Graphics.getColor(rgb);
+               Color c = new Color(display, rgb);
+               colorRegistry.put(key, c);        
+               return c;
+       }
+
+       /**
+        * Creates a color that can be used for areas of the form that is inactive.
+        * These areas can contain images, links, controls and other content but are
+        * considered auxilliary to the main content area.
+        * 
+        * <p>
+        * The color should not be disposed because it is managed by this class.
+        * 
+        * @return the inactive form color
+        */
+       public Color getInactiveBackground() {
+               String key = "__ncbg__"; //$NON-NLS-1$
+               Color color = getColor(key);
+               if (color == null) {
+                       RGB sel = getSystemColor(SWT.COLOR_LIST_SELECTION);
+                       // a blend of 95% white and 5% list selection system color
+                       RGB ncbg = blend(sel, getSystemColor(SWT.COLOR_WHITE), 5);
+                       color = createColor(key, ncbg);
+               }
+               return color;
+       }
+
+       /**
+        * Creates the color for the specified key using the provided RGB values.
+        * The color object will be returned and also put into the registry. If
+        * there is already another color object under the same key in the registry,
+        * the existing object will be disposed. When the class is disposed, the
+        * color will be disposed with it.
+        * 
+        * @param key
+        *            the unique color key
+        * @param r
+        *            red value
+        * @param g
+        *            green value
+        * @param b
+        *            blue value
+        * @return the allocated color object
+        */
+       public Color createColor(String key, int r, int g, int b) {
+               return createColor(key, new RGB(r,g,b));
+       }
+
+       /**
+        * Computes the border color relative to the background. Allocated border
+        * color is designed to work well with white. Otherwise, stanard widget
+        * background color will be used.
+        */
+       protected void updateBorderColor() {
+               if (isWhiteBackground())
+                       border = getColor(IFormColors.BORDER);
+               else {
+                       border = display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
+                       Color bg = getImpliedBackground();
+                       if (border.getRed() == bg.getRed()
+                                       && border.getGreen() == bg.getGreen()
+                                       && border.getBlue() == bg.getBlue())
+                               border = display.getSystemColor(SWT.COLOR_WIDGET_DARK_SHADOW);
+               }
+       }
+
+       /**
+        * Sets the background color. All the toolkits that use this class will
+        * share the same background.
+        * 
+        * @param bg
+        *            background color
+        */
+       public void setBackground(Color bg) {
+               this.background = bg;
+               updateBorderColor();
+               updateFormHeaderColors();
+       }
+
+       /**
+        * Sets the foreground color. All the toolkits that use this class will
+        * share the same foreground.
+        * 
+        * @param fg
+        *            foreground color
+        */
+       public void setForeground(Color fg) {
+               this.foreground = fg;
+       }
+
+       /**
+        * Returns the current background color.
+        * 
+        * @return the background color
+        */
+       public Color getBackground() {
+               return background;
+       }
+
+       /**
+        * Returns the current foreground color.
+        * 
+        * @return the foreground color
+        */
+       public Color getForeground() {
+               return foreground;
+       }
+
+       /**
+        * Returns the computed border color. Border color depends on the background
+        * and is recomputed whenever the background changes.
+        * 
+        * @return the current border color
+        */
+       public Color getBorderColor() {
+               return border;
+       }
+
+       /**
+        * Tests if the background is white. White background has RGB value
+        * 255,255,255.
+        * 
+        * @return <samp>true</samp> if background is white, <samp>false</samp>
+        *         otherwise.
+        */
+       public boolean isWhiteBackground() {
+               Color bg = getImpliedBackground();
+               return bg.getRed() == 255 && bg.getGreen() == 255
+                               && bg.getBlue() == 255;
+       }
+
+       /**
+        * Returns the color object for the provided key or <samp>null </samp> if
+        * not in the registry.
+        * 
+        * @param key
+        *            the color key
+        * @return color object if found, or <samp>null </samp> if not.
+        */
+       public Color getColor(String key) {
+               if (key.startsWith(IFormColors.TB_PREFIX))
+                       initializeSectionToolBarColors();
+               else if (key.startsWith(IFormColors.H_PREFIX))
+                       initializeFormHeaderColors();
+               return (Color) colorRegistry.get(key);
+       }
+
+       /**
+        * Disposes all the colors in the registry.
+        */
+       public void dispose() {
+               if (resources != null)
+                       resources.dispose();
+               resources = null;
+               colorRegistry = null;
+       }
+
+       /**
+        * Marks the colors shared. This prevents toolkits that share this object
+        * from disposing it.
+        */
+       public void markShared() {
+               this.shared = true;
+       }
+
+       /**
+        * Tests if the colors are shared.
+        * 
+        * @return <code>true</code> if shared, <code>false</code> otherwise.
+        */
+       public boolean isShared() {
+               return shared;
+       }
+
+       /**
+        * Blends c1 and c2 based in the provided ratio.
+        * 
+        * @param c1
+        *            first color
+        * @param c2
+        *            second color
+        * @param ratio
+        *            percentage of the first color in the blend (0-100)
+        * @return the RGB value of the blended color
+        */
+       public static RGB blend(RGB c1, RGB c2, int ratio) {
+               int r = blend(c1.red, c2.red, ratio);
+               int g = blend(c1.green, c2.green, ratio);
+               int b = blend(c1.blue, c2.blue, ratio);
+               return new RGB(r, g, b);
+       }
+
+       /**
+        * Tests the source RGB for range.
+        * 
+        * @param rgb
+        *            the tested RGB
+        * @param from
+        *            range start (excluding the value itself)
+        * @param to
+        *            range end (excluding the value itself)
+        * @return <code>true</code> if at least one of the primary colors in the
+        *         source RGB are within the provided range, <code>false</code>
+        *         otherwise.
+        */
+       public static boolean testAnyPrimaryColor(RGB rgb, int from, int to) {
+               if (testPrimaryColor(rgb.red, from, to))
+                       return true;
+               if (testPrimaryColor(rgb.green, from, to))
+                       return true;
+               if (testPrimaryColor(rgb.blue, from, to))
+                       return true;
+               return false;
+       }
+
+       /**
+        * Tests the source RGB for range.
+        * 
+        * @param rgb
+        *            the tested RGB
+        * @param from
+        *            range start (excluding the value itself)
+        * @param to
+        *            tange end (excluding the value itself)
+        * @return <code>true</code> if at least two of the primary colors in the
+        *         source RGB are within the provided range, <code>false</code>
+        *         otherwise.
+        */
+       public static boolean testTwoPrimaryColors(RGB rgb, int from, int to) {
+               int total = 0;
+               if (testPrimaryColor(rgb.red, from, to))
+                       total++;
+               if (testPrimaryColor(rgb.green, from, to))
+                       total++;
+               if (testPrimaryColor(rgb.blue, from, to))
+                       total++;
+               return total >= 2;
+       }
+
+       /**
+        * Blends two primary color components based on the provided ratio.
+        * 
+        * @param v1
+        *            first component
+        * @param v2
+        *            second component
+        * @param ratio
+        *            percentage of the first component in the blend
+        * @return
+        */
+       private static int blend(int v1, int v2, int ratio) {
+               int b = (ratio * v1 + (100 - ratio) * v2) / 100;
+               return Math.min(255, b);
+       }
+
+       private Color getImpliedBackground() {
+               if (getBackground() != null)
+                       return getBackground();
+               return getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
+       }
+
+       private static boolean testPrimaryColor(int value, int from, int to) {
+               return value > from && value < to;
+       }
+
+       private void createTitleColor() {
+               /*
+                * RGB rgb = getSystemColor(SWT.COLOR_LIST_SELECTION); // test too light
+                * if (testTwoPrimaryColors(rgb, 120, 151)) rgb = blend(rgb, BLACK, 80);
+                * else if (testTwoPrimaryColors(rgb, 150, 256)) rgb = blend(rgb, BLACK,
+                * 50); createColor(TITLE, rgb);
+                */
+               RGB bg = getImpliedBackground().getRGB();
+               RGB listSelection = getSystemColor(SWT.COLOR_LIST_SELECTION);
+               RGB listForeground = getSystemColor(SWT.COLOR_LIST_FOREGROUND);
+               RGB rgb = listSelection;
+
+               // Group 1
+               // Rule: If at least 2 of the LIST_SELECTION RGB values are equal to or
+               // between 0 and 120, then use 100% LIST_SELECTION as it is (no
+               // additions)
+               // Examples: XP Default, Win Classic Standard, Win High Con White, Win
+               // Classic Marine
+               if (testTwoPrimaryColors(listSelection, -1, 121))
+                       rgb = listSelection;
+               // Group 2
+               // When LIST_BACKGROUND = white (255, 255, 255) or not black, text
+               // colour = LIST_SELECTION @ 100% Opacity + 50% LIST_FOREGROUND over
+               // LIST_BACKGROUND
+               // Rule: If at least 2 of the LIST_SELECTION RGB values are equal to or
+               // between 121 and 255, then add 50% LIST_FOREGROUND to LIST_SELECTION
+               // foreground colour
+               // Examples: Win Vista, XP Silver, XP Olive , Win Classic Plum, OSX
+               // Aqua, OSX Graphite, Linux GTK
+               else if (testTwoPrimaryColors(listSelection, 120, 256)
+                               || (bg.red == 0 && bg.green == 0 && bg.blue == 0))
+                       rgb = blend(listSelection, listForeground, 50);
+               // Group 3
+               // When LIST_BACKGROUND = black (0, 0, 0), text colour = LIST_SELECTION
+               // @ 100% Opacity + 50% LIST_FOREGROUND over LIST_BACKGROUND
+               // Rule: If LIST_BACKGROUND = 0, 0, 0, then add 50% LIST_FOREGROUND to
+               // LIST_SELECTION foreground colour
+               // Examples: Win High Con Black, Win High Con #1, Win High Con #2
+               // (covered in the second part of the OR clause above)
+               createColor(IFormColors.TITLE, rgb);
+       }
+
+       private void createTwistieColors() {
+               RGB rgb = getColor(IFormColors.TITLE).getRGB();
+               RGB white = getSystemColor(SWT.COLOR_WHITE);
+               createColor(TB_TOGGLE, rgb);
+               rgb = blend(rgb, white, 60);
+               createColor(TB_TOGGLE_HOVER, rgb);
+       }
+
+       private void createTitleBarGradientColors() {
+               RGB tbBg = getSystemColor(SWT.COLOR_TITLE_BACKGROUND);
+               RGB bg = getImpliedBackground().getRGB();
+
+               // Group 1
+               // Rule: If at least 2 of the RGB values are equal to or between 180 and
+               // 255, then apply specified opacity for Group 1
+               // Examples: Vista, XP Silver, Wn High Con #2
+               // Gradient Bottom = TITLE_BACKGROUND @ 30% Opacity over LIST_BACKGROUND
+               // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+               if (testTwoPrimaryColors(tbBg, 179, 256))
+                       tbBg = blend(tbBg, bg, 30);
+
+               // Group 2
+               // Rule: If at least 2 of the RGB values are equal to or between 121 and
+               // 179, then apply specified opacity for Group 2
+               // Examples: XP Olive, OSX Graphite, Linux GTK, Wn High Con Black
+               // Gradient Bottom = TITLE_BACKGROUND @ 20% Opacity over LIST_BACKGROUND
+               // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+               else if (testTwoPrimaryColors(tbBg, 120, 180))
+                       tbBg = blend(tbBg, bg, 20);
+
+               // Group 3
+               // Rule: Everything else
+               // Examples: XP Default, Wn Classic Standard, Wn Marine, Wn Plum, OSX
+               // Aqua, Wn High Con White, Wn High Con #1
+               // Gradient Bottom = TITLE_BACKGROUND @ 10% Opacity over LIST_BACKGROUND
+               // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+               else {
+                       tbBg = blend(tbBg, bg, 10);
+               }
+
+               createColor(IFormColors.TB_BG, tbBg);
+               
+               // for backward compatibility
+               createColor(TB_GBG, tbBg);
+       }
+
+       private void createTitleBarOutlineColors() {
+               // title bar outline - border color
+               RGB tbBorder = getSystemColor(SWT.COLOR_TITLE_BACKGROUND);
+               RGB bg = getImpliedBackground().getRGB();
+               // Group 1
+               // Rule: If at least 2 of the RGB values are equal to or between 180 and
+               // 255, then apply specified opacity for Group 1
+               // Examples: Vista, XP Silver, Wn High Con #2
+               // Keyline = TITLE_BACKGROUND @ 70% Opacity over LIST_BACKGROUND
+               if (testTwoPrimaryColors(tbBorder, 179, 256))
+                       tbBorder = blend(tbBorder, bg, 70);
+
+               // Group 2
+               // Rule: If at least 2 of the RGB values are equal to or between 121 and
+               // 179, then apply specified opacity for Group 2
+               // Examples: XP Olive, OSX Graphite, Linux GTK, Wn High Con Black
+
+               // Keyline = TITLE_BACKGROUND @ 50% Opacity over LIST_BACKGROUND
+               else if (testTwoPrimaryColors(tbBorder, 120, 180))
+                       tbBorder = blend(tbBorder, bg, 50);
+
+               // Group 3
+               // Rule: Everything else
+               // Examples: XP Default, Wn Classic Standard, Wn Marine, Wn Plum, OSX
+               // Aqua, Wn High Con White, Wn High Con #1
+
+               // Keyline = TITLE_BACKGROUND @ 30% Opacity over LIST_BACKGROUND
+               else {
+                       tbBorder = blend(tbBorder, bg, 30);
+               }
+               createColor(FormColors.TB_BORDER, tbBorder);
+       }
+
+       private void updateFormHeaderColors() {
+               if (colorRegistry.containsKey(IFormColors.H_GRADIENT_END)) {
+                       disposeIfFound(IFormColors.H_GRADIENT_END);
+                       disposeIfFound(IFormColors.H_GRADIENT_START);
+                       disposeIfFound(IFormColors.H_BOTTOM_KEYLINE1);
+                       disposeIfFound(IFormColors.H_BOTTOM_KEYLINE2);
+                       disposeIfFound(IFormColors.H_HOVER_LIGHT);
+                       disposeIfFound(IFormColors.H_HOVER_FULL);
+                       initializeFormHeaderColors();
+               }
+       }
+
+       private void disposeIfFound(String key) {
+               Color color = getColor(key);
+               if (color != null) {
+                       colorRegistry.remove(key);
+               // RAP [rh] changes due to missing Color#dispose()                      
+//                     color.dispose();
+               }
+       }
+
+       private void createFormHeaderColors() {
+               createFormHeaderGradientColors();
+               createFormHeaderKeylineColors();
+               createFormHeaderDNDColors();
+       }
+
+       private void createFormHeaderGradientColors() {
+               RGB titleBg = getSystemColor(SWT.COLOR_TITLE_BACKGROUND);
+               Color bgColor = getImpliedBackground();
+               RGB bg = bgColor.getRGB();
+               RGB bottom, top;
+               // Group 1
+               // Rule: If at least 2 of the RGB values are equal to or between 180 and
+               // 255, then apply specified opacity for Group 1
+               // Examples: Vista, XP Silver, Wn High Con #2
+               // Gradient Bottom = TITLE_BACKGROUND @ 30% Opacity over LIST_BACKGROUND
+               // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+               if (testTwoPrimaryColors(titleBg, 179, 256)) {
+                       bottom = blend(titleBg, bg, 30);
+                       top = bg;
+               }
+
+               // Group 2
+               // Rule: If at least 2 of the RGB values are equal to or between 121 and
+               // 179, then apply specified opacity for Group 2
+               // Examples: XP Olive, OSX Graphite, Linux GTK, Wn High Con Black
+               // Gradient Bottom = TITLE_BACKGROUND @ 20% Opacity over LIST_BACKGROUND
+               // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+               else if (testTwoPrimaryColors(titleBg, 120, 180)) {
+                       bottom = blend(titleBg, bg, 20);
+                       top = bg;
+               }
+
+               // Group 3
+               // Rule: If at least 2 of the RGB values are equal to or between 0 and
+               // 120, then apply specified opacity for Group 3
+               // Examples: XP Default, Wn Classic Standard, Wn Marine, Wn Plum, OSX
+               // Aqua, Wn High Con White, Wn High Con #1
+               // Gradient Bottom = TITLE_BACKGROUND @ 10% Opacity over LIST_BACKGROUND
+               // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+               else {
+                       bottom = blend(titleBg, bg, 10);
+                       top = bg;
+               }
+               createColor(IFormColors.H_GRADIENT_END, top);
+               createColor(IFormColors.H_GRADIENT_START, bottom);
+       }
+
+       private void createFormHeaderKeylineColors() {
+               RGB titleBg = getSystemColor(SWT.COLOR_TITLE_BACKGROUND);
+               Color bgColor = getImpliedBackground();
+               RGB bg = bgColor.getRGB();
+               RGB keyline2;
+               // H_BOTTOM_KEYLINE1
+               createColor(IFormColors.H_BOTTOM_KEYLINE1, new RGB(255, 255, 255));
+
+               // H_BOTTOM_KEYLINE2
+               // Group 1
+               // Rule: If at least 2 of the RGB values are equal to or between 180 and
+               // 255, then apply specified opacity for Group 1
+               // Examples: Vista, XP Silver, Wn High Con #2
+               // Keyline = TITLE_BACKGROUND @ 70% Opacity over LIST_BACKGROUND
+               if (testTwoPrimaryColors(titleBg, 179, 256))
+                       keyline2 = blend(titleBg, bg, 70);
+
+               // Group 2
+               // Rule: If at least 2 of the RGB values are equal to or between 121 and
+               // 179, then apply specified opacity for Group 2
+               // Examples: XP Olive, OSX Graphite, Linux GTK, Wn High Con Black
+               // Keyline = TITLE_BACKGROUND @ 50% Opacity over LIST_BACKGROUND
+               else if (testTwoPrimaryColors(titleBg, 120, 180))
+                       keyline2 = blend(titleBg, bg, 50);
+
+               // Group 3
+               // Rule: If at least 2 of the RGB values are equal to or between 0 and
+               // 120, then apply specified opacity for Group 3
+               // Examples: XP Default, Wn Classic Standard, Wn Marine, Wn Plum, OSX
+               // Aqua, Wn High Con White, Wn High Con #1
+
+               // Keyline = TITLE_BACKGROUND @ 30% Opacity over LIST_BACKGROUND
+               else
+                       keyline2 = blend(titleBg, bg, 30);
+               // H_BOTTOM_KEYLINE2
+               createColor(IFormColors.H_BOTTOM_KEYLINE2, keyline2);
+       }
+
+       private void createFormHeaderDNDColors() {
+               RGB titleBg = getSystemColor(SWT.COLOR_TITLE_BACKGROUND_GRADIENT);
+               Color bgColor = getImpliedBackground();
+               RGB bg = bgColor.getRGB();
+               RGB light, full;
+               // ALL Themes
+               //
+               // Light Highlight
+               // When *near* the 'hot' area
+               // Rule: If near the title in the 'hot' area, show background highlight
+               // TITLE_BACKGROUND_GRADIENT @ 40%
+               light = blend(titleBg, bg, 40);
+               // Full Highlight
+               // When *on* the title area (regions 1 and 2)
+               // Rule: If near the title in the 'hot' area, show background highlight
+               // TITLE_BACKGROUND_GRADIENT @ 60%
+               full = blend(titleBg, bg, 60);
+               // H_DND_LIGHT
+               // H_DND_FULL
+               createColor(IFormColors.H_HOVER_LIGHT, light);
+               createColor(IFormColors.H_HOVER_FULL, full);
+       }
+       
+       private LocalResourceManager getResourceManager() {
+               if (resources == null)
+                       resources = new LocalResourceManager(JFaceResources.getResources());
+               return resources;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormFonts.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormFonts.java
new file mode 100644 (file)
index 0000000..9e931ba
--- /dev/null
@@ -0,0 +1,122 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+import java.util.HashMap;
+
+import org.eclipse.jface.resource.DeviceResourceException;
+import org.eclipse.jface.resource.FontDescriptor;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.resource.LocalResourceManager;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+//import org.eclipse.swt.internal.graphics.Graphics;
+import org.eclipse.swt.widgets.Display;
+
+public class FormFonts {
+       private static FormFonts instance;
+
+       public static FormFonts getInstance() {
+               if (instance == null)
+                       instance = new FormFonts();
+               return instance;
+       }
+
+       private LocalResourceManager resources;
+       private HashMap descriptors;
+
+       private FormFonts() {
+       }
+
+       private class BoldFontDescriptor extends FontDescriptor {
+               private FontData[] fFontData;
+
+               BoldFontDescriptor(Font font) {
+                       // RAP [if] Changes due to different way of creating fonts
+                       // fFontData = font.getFontData();
+                       // for (int i = 0; i < fFontData.length; i++) {
+                       // fFontData[i].setStyle(fFontData[i].getStyle() | SWT.BOLD);
+                       // }
+                       FontData fontData = font.getFontData()[0];
+                       // Font boldFont = Graphics.getFont( fontData.getName(),
+                       // fontData.getHeight(),
+                       // fontData.getStyle() | SWT.BOLD );
+                       Font boldFont = new Font(Display.getCurrent(), fontData.getName(), fontData.getHeight(),
+                                       fontData.getStyle() | SWT.BOLD);
+                       fFontData = boldFont.getFontData();
+               }
+
+               public boolean equals(Object obj) {
+                       if (obj instanceof BoldFontDescriptor) {
+                               BoldFontDescriptor desc = (BoldFontDescriptor) obj;
+                               if (desc.fFontData.length != fFontData.length)
+                                       return false;
+                               for (int i = 0; i < fFontData.length; i++)
+                                       if (!fFontData[i].equals(desc.fFontData[i]))
+                                               return false;
+                               return true;
+                       }
+                       return false;
+               }
+
+               public int hashCode() {
+                       int hash = 0;
+                       for (int i = 0; i < fFontData.length; i++)
+                               hash = hash * 7 + fFontData[i].hashCode();
+                       return hash;
+               }
+
+               public Font createFont(Device device) throws DeviceResourceException {
+                       // RAP [if] Changes due to different way of creating fonts
+                       return new Font(device, fFontData[0]);
+                       // return Graphics.getFont( fFontData[ 0 ] );
+               }
+
+               public void destroyFont(Font previouslyCreatedFont) {
+                       // RAP [if] unnecessary
+                       // previouslyCreatedFont.dispose();
+               }
+       }
+
+       public Font getBoldFont(Display display, Font font) {
+               checkHashMaps();
+               BoldFontDescriptor desc = new BoldFontDescriptor(font);
+               Font result = getResourceManager().createFont(desc);
+               descriptors.put(result, desc);
+               return result;
+       }
+
+       public boolean markFinished(Font boldFont) {
+               checkHashMaps();
+               BoldFontDescriptor desc = (BoldFontDescriptor) descriptors.get(boldFont);
+               if (desc != null) {
+                       getResourceManager().destroyFont(desc);
+                       if (getResourceManager().find(desc) == null) {
+                               descriptors.remove(boldFont);
+                               validateHashMaps();
+                       }
+                       return true;
+
+               }
+               // if the image was not found, dispose of it for the caller
+               // RAP [if] unnecessary
+               // boldFont.dispose();
+               return false;
+       }
+
+       private LocalResourceManager getResourceManager() {
+               if (resources == null)
+                       resources = new LocalResourceManager(JFaceResources.getResources());
+               return resources;
+       }
+
+       private void checkHashMaps() {
+               if (descriptors == null)
+                       descriptors = new HashMap();
+       }
+
+       private void validateHashMaps() {
+               if (descriptors.size() == 0)
+                       descriptors = null;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormToolkit.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormToolkit.java
new file mode 100644 (file)
index 0000000..9927104
--- /dev/null
@@ -0,0 +1,913 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+//import org.eclipse.swt.custom.CCombo;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.FocusAdapter;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.KeyAdapter;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+// RAP [rh] Paint events missing
+//import org.eclipse.swt.events.PaintEvent;
+//import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+//RAP [rh] GC missing
+//import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Point;
+//import org.eclipse.swt.graphics.RGB;
+//import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+//import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+//import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.Widget;
+//import org.eclipse.ui.forms.FormColors;
+//import org.eclipse.ui.forms.HyperlinkGroup;
+//import org.eclipse.ui.forms.IFormColors;
+//import org.eclipse.ui.internal.forms.widgets.FormFonts;
+//import org.eclipse.ui.internal.forms.widgets.FormUtil;
+
+/**
+ * The toolkit is responsible for creating SWT controls adapted to work in
+ * Eclipse forms. In addition to changing their presentation properties (fonts,
+ * colors etc.), various listeners are attached to make them behave correctly in
+ * the form context.
+ * <p>
+ * In addition to being the control factory, the toolkit is also responsible for
+ * painting flat borders for select controls, managing hyperlink groups and
+ * control colors.
+ * <p>
+ * The toolkit creates some of the most common controls used to populate Eclipse
+ * forms. Controls that must be created using their constructors,
+ * <code>adapt()</code> method is available to change its properties in the
+ * same way as with the supported toolkit controls.
+ * <p>
+ * Typically, one toolkit object is created per workbench part (for example, an
+ * editor or a form wizard). The toolkit is disposed when the part is disposed.
+ * To conserve resources, it is possible to create one color object for the
+ * entire plug-in and share it between several toolkits. The plug-in is
+ * responsible for disposing the colors (disposing the toolkit that uses shared
+ * color object will not dispose the colors).
+ * <p>
+ * FormToolkit is normally instantiated, but can also be subclassed if some of
+ * the methods needs to be modified. In those cases, <code>super</code> must
+ * be called to preserve normal behaviour.
+ *
+ * @since 1.0
+ */
+public class FormToolkit {
+       public static final String KEY_DRAW_BORDER = "FormWidgetFactory.drawBorder"; //$NON-NLS-1$
+
+       public static final String TREE_BORDER = "treeBorder"; //$NON-NLS-1$
+
+       public static final String TEXT_BORDER = "textBorder"; //$NON-NLS-1$
+
+       private int borderStyle = SWT.NULL;
+
+       private FormColors colors;
+
+       private int orientation = Window.getDefaultOrientation();
+
+       // private KeyListener deleteListener;
+       // RAP [rh] Paint events missing
+//     private BorderPainter borderPainter;
+
+       private BoldFontHolder boldFontHolder;
+
+//     private HyperlinkGroup hyperlinkGroup;
+       
+       private boolean isDisposed = false;
+
+       /* default */
+       VisibilityHandler visibilityHandler;
+
+       /* default */
+       KeyboardHandler keyboardHandler;
+
+       // RAP [rh] Paint events missing
+//     private class BorderPainter implements PaintListener {
+//             public void paintControl(PaintEvent event) {
+//                     Composite composite = (Composite) event.widget;
+//                     Control[] children = composite.getChildren();
+//                     for (int i = 0; i < children.length; i++) {
+//                             Control c = children[i];
+//                             boolean inactiveBorder = false;
+//                             boolean textBorder = false;
+//                             if (!c.isVisible())
+//                                     continue;
+//                             /*
+//                              * if (c.getEnabled() == false && !(c instanceof CCombo))
+//                              * continue;
+//                              */
+//                             if (c instanceof Hyperlink)
+//                                     continue;
+//                             Object flag = c.getData(KEY_DRAW_BORDER);
+//                             if (flag != null) {
+//                                     if (flag.equals(Boolean.FALSE))
+//                                             continue;
+//                                     if (flag.equals(TREE_BORDER))
+//                                             inactiveBorder = true;
+//                                     else if (flag.equals(TEXT_BORDER))
+//                                             textBorder = true;
+//                             }
+//                             if (getBorderStyle() == SWT.BORDER) {
+//                                     if (!inactiveBorder && !textBorder) {
+//                                             continue;
+//                                     }
+//                                     if (c instanceof Text || c instanceof Table
+//                                                     || c instanceof Tree)
+//                                             continue;
+//                             }
+//                             if (!inactiveBorder
+//                                             && (c instanceof Text || c instanceof CCombo || textBorder)) {
+//                                     Rectangle b = c.getBounds();
+//                                     GC gc = event.gc;
+//                                     gc.setForeground(c.getBackground());
+//                                     gc.drawRectangle(b.x - 1, b.y - 1, b.width + 1,
+//                                                     b.height + 1);
+//                                     // gc.setForeground(getBorderStyle() == SWT.BORDER ? colors
+//                                     // .getBorderColor() : colors.getForeground());
+//                                     gc.setForeground(colors.getBorderColor());
+//                                     if (c instanceof CCombo)
+//                                             gc.drawRectangle(b.x - 1, b.y - 1, b.width + 1,
+//                                                             b.height + 1);
+//                                     else
+//                                             gc.drawRectangle(b.x - 1, b.y - 2, b.width + 1,
+//                                                             b.height + 3);
+//                             } else if (inactiveBorder || c instanceof Table
+//                                             || c instanceof Tree) {
+//                                     Rectangle b = c.getBounds();
+//                                     GC gc = event.gc;
+//                                     gc.setForeground(colors.getBorderColor());
+//                                     gc.drawRectangle(b.x - 1, b.y - 1, b.width + 1,
+//                                                     b.height + 1);
+//                             }
+//                     }
+//             }
+//     }
+
+       private static class VisibilityHandler extends FocusAdapter {
+               public void focusGained(FocusEvent e) {
+                       Widget w = e.widget;
+                       if (w instanceof Control) {
+                               FormUtil.ensureVisible((Control) w);
+                       }
+               }
+       }
+
+       private static class KeyboardHandler extends KeyAdapter {
+               public void keyPressed(KeyEvent e) {
+                       Widget w = e.widget;
+                       if (w instanceof Control) {
+                               if (e.doit)
+                                       FormUtil.processKey(e.keyCode, (Control) w);
+                       }
+               }
+       }
+
+       private class BoldFontHolder {
+               private Font normalFont;
+
+               private Font boldFont;
+
+               public BoldFontHolder() {
+               }
+
+               public Font getBoldFont(Font font) {
+                       createBoldFont(font);
+                       return boldFont;
+               }
+
+               private void createBoldFont(Font font) {
+                       if (normalFont == null || !normalFont.equals(font)) {
+                               normalFont = font;
+                               dispose();
+                       }
+                       if (boldFont == null) {
+                               boldFont = FormFonts.getInstance().getBoldFont(colors.getDisplay(),
+                                               normalFont);
+                       }
+               }
+
+               public void dispose() {
+                       if (boldFont != null) {
+                               FormFonts.getInstance().markFinished(boldFont);
+                               boldFont = null;
+                       }
+               }
+       }
+
+       /**
+        * Creates a toolkit that is self-sufficient (will manage its own colors).
+        * <p>
+        * Clients that call this method must call {@link #dispose()} when they
+        * are finished using the toolkit.
+        *
+        */
+       public FormToolkit(Display display) {
+               this(new FormColors(display));
+       }
+
+       /**
+        * Creates a toolkit that will use the provided (shared) colors. The toolkit
+        * will dispose the colors if and only if they are <b>not</b> marked as
+        * shared via the <code>markShared()</code> method.
+        * <p>
+        * Clients that call this method must call {@link #dispose()} when they
+        * are finished using the toolkit.
+        *
+        * @param colors
+        *            the shared colors
+        */
+       public FormToolkit(FormColors colors) {
+               this.colors = colors;
+               initialize();
+       }
+
+       /**
+        * Creates a button as a part of the form.
+        *
+        * @param parent
+        *            the button parent
+        * @param text
+        *            an optional text for the button (can be <code>null</code>)
+        * @param style
+        *            the button style (for example, <code>SWT.PUSH</code>)
+        * @return the button widget
+        */
+       public Button createButton(Composite parent, String text, int style) {
+               Button button = new Button(parent, style | SWT.FLAT | orientation);
+               if (text != null)
+                       button.setText(text);
+               adapt(button, true, true);
+               return button;
+       }
+
+       /**
+        * Creates the composite as a part of the form.
+        *
+        * @param parent
+        *            the composite parent
+        * @return the composite widget
+        */
+       public Composite createComposite(Composite parent) {
+               return createComposite(parent, SWT.NULL);
+       }
+
+       /**
+        * Creates the composite as part of the form using the provided style.
+        *
+        * @param parent
+        *            the composite parent
+        * @param style
+        *            the composite style
+        * @return the composite widget
+        */
+       public Composite createComposite(Composite parent, int style) {
+//             Composite composite = new LayoutComposite(parent, style | orientation);
+               Composite composite = new Composite(parent, style | orientation);
+               adapt(composite);
+               return composite;
+       }
+
+       /**
+        * Creats the composite that can server as a separator between various parts
+        * of a form. Separator height should be controlled by setting the height
+        * hint on the layout data for the composite.
+        *
+        * @param parent
+        *            the separator parent
+        * @return the separator widget
+        */
+// RAP [rh] createCompositeSeparator: currently no useful implementation possible, delete?
+       public Composite createCompositeSeparator(Composite parent) {
+               final Composite composite = new Composite(parent, orientation);
+// RAP [rh] GC and paint events missing
+//             composite.addListener(SWT.Paint, new Listener() {
+//                     public void handleEvent(Event e) {
+//                             if (composite.isDisposed())
+//                                     return;
+//                             Rectangle bounds = composite.getBounds();
+//                             GC gc = e.gc;
+//                             gc.setForeground(colors.getColor(IFormColors.SEPARATOR));
+//                             if (colors.getBackground() != null)
+//                                     gc.setBackground(colors.getBackground());
+//                             gc.fillGradientRectangle(0, 0, bounds.width, bounds.height,
+//                                             false);
+//                     }
+//             });
+//             if (parent instanceof Section)
+//                     ((Section) parent).setSeparatorControl(composite);
+               return composite;
+       }
+
+       /**
+        * Creates a label as a part of the form.
+        *
+        * @param parent
+        *            the label parent
+        * @param text
+        *            the label text
+        * @return the label widget
+        */
+       public Label createLabel(Composite parent, String text) {
+               return createLabel(parent, text, SWT.NONE);
+       }
+
+       /**
+        * Creates a label as a part of the form.
+        *
+        * @param parent
+        *            the label parent
+        * @param text
+        *            the label text
+        * @param style
+        *            the label style
+        * @return the label widget
+        */
+       public Label createLabel(Composite parent, String text, int style) {
+               Label label = new Label(parent, style | orientation);
+               if (text != null)
+                       label.setText(text);
+               adapt(label, false, false);
+               return label;
+       }
+
+       /**
+        * Creates a hyperlink as a part of the form. The hyperlink will be added to
+        * the hyperlink group that belongs to this toolkit.
+        *
+        * @param parent
+        *            the hyperlink parent
+        * @param text
+        *            the text of the hyperlink
+        * @param style
+        *            the hyperlink style
+        * @return the hyperlink widget
+        */
+//     public Hyperlink createHyperlink(Composite parent, String text, int style) {
+//             Hyperlink hyperlink = new Hyperlink(parent, style | orientation);
+//             if (text != null)
+//                     hyperlink.setText(text);
+//             hyperlink.addFocusListener(visibilityHandler);
+//             hyperlink.addKeyListener(keyboardHandler);
+//             hyperlinkGroup.add(hyperlink);
+//             return hyperlink;
+//     }
+
+       /**
+        * Creates an image hyperlink as a part of the form. The hyperlink will be
+        * added to the hyperlink group that belongs to this toolkit.
+        *
+        * @param parent
+        *            the hyperlink parent
+        * @param style
+        *            the hyperlink style
+        * @return the image hyperlink widget
+        */
+//     public ImageHyperlink createImageHyperlink(Composite parent, int style) {
+//             ImageHyperlink hyperlink = new ImageHyperlink(parent, style
+//                             | orientation);
+//             hyperlink.addFocusListener(visibilityHandler);
+//             hyperlink.addKeyListener(keyboardHandler);
+//             hyperlinkGroup.add(hyperlink);
+//             return hyperlink;
+//     }
+
+       /**
+        * Creates a rich text as a part of the form.
+        *
+        * @param parent
+        *            the rich text parent
+        * @param trackFocus
+        *            if <code>true</code>, the toolkit will monitor focus
+        *            transfers to ensure that the hyperlink in focus is visible in
+        *            the form.
+        * @return the rich text widget
+        * @since 1.2
+        */
+//     public FormText createFormText(Composite parent, boolean trackFocus) {
+//             FormText engine = new FormText(parent, SWT.WRAP | orientation);
+//             engine.marginWidth = 1;
+//             engine.marginHeight = 0;
+//             engine.setHyperlinkSettings(getHyperlinkGroup());
+//             adapt(engine, trackFocus, true);
+//             engine.setMenu(parent.getMenu());
+//             return engine;
+//     }
+
+       /**
+        * Adapts a control to be used in a form that is associated with this
+        * toolkit. This involves adjusting colors and optionally adding handlers to
+        * ensure focus tracking and keyboard management.
+        *
+        * @param control
+        *            a control to adapt
+        * @param trackFocus
+        *            if <code>true</code>, form will be scrolled horizontally
+        *            and/or vertically if needed to ensure that the control is
+        *            visible when it gains focus. Set it to <code>false</code> if
+        *            the control is not capable of gaining focus.
+        * @param trackKeyboard
+        *            if <code>true</code>, the control that is capable of
+        *            gaining focus will be tracked for certain keys that are
+        *            important to the underlying form (for example, PageUp,
+        *            PageDown, ScrollUp, ScrollDown etc.). Set it to
+        *            <code>false</code> if the control is not capable of gaining
+        *            focus or these particular key event are already used by the
+        *            control.
+        */
+       public void adapt(Control control, boolean trackFocus, boolean trackKeyboard) {
+               control.setBackground(colors.getBackground());
+               control.setForeground(colors.getForeground());
+//             if (control instanceof ExpandableComposite) {
+//                     ExpandableComposite ec = (ExpandableComposite) control;
+//                     if (ec.toggle != null) {
+//                             if (trackFocus)
+//                                     ec.toggle.addFocusListener(visibilityHandler);
+//                             if (trackKeyboard)
+//                                     ec.toggle.addKeyListener(keyboardHandler);
+//                     }
+//                     if (ec.textLabel != null) {
+//                             if (trackFocus)
+//                                     ec.textLabel.addFocusListener(visibilityHandler);
+//                             if (trackKeyboard)
+//                                     ec.textLabel.addKeyListener(keyboardHandler);
+//                     }
+//                     return;
+//             }
+               if (trackFocus)
+                       control.addFocusListener(visibilityHandler);
+               if (trackKeyboard)
+                       control.addKeyListener(keyboardHandler);
+       }
+
+       /**
+        * Adapts a composite to be used in a form associated with this toolkit.
+        *
+        * @param composite
+        *            the composite to adapt
+        */
+       public void adapt(Composite composite) {
+               composite.setBackground(colors.getBackground());
+               composite.addMouseListener(new MouseAdapter() {
+                       public void mouseDown(MouseEvent e) {
+                               ((Control) e.widget).setFocus();
+                       }
+               });
+               if (composite.getParent() != null)
+                       composite.setMenu(composite.getParent().getMenu());
+       }
+
+       /**
+        * A helper method that ensures the provided control is visible when
+        * ScrolledComposite is somewhere in the parent chain. If scroll bars are
+        * visible and the control is clipped, the client of the scrolled composite
+        * will be scrolled to reveal the control.
+        *
+        * @param c
+        *            the control to reveal
+        */
+       public static void ensureVisible(Control c) {
+               FormUtil.ensureVisible(c);
+       }
+
+       /**
+        * Creates a section as a part of the form.
+        *
+        * @param parent
+        *            the section parent
+        * @param sectionStyle
+        *            the section style
+        * @return the section widget
+        */
+//     public Section createSection(Composite parent, int sectionStyle) {
+//             Section section = new Section(parent, orientation, sectionStyle);
+//             section.setMenu(parent.getMenu());
+//             adapt(section, true, true);
+//             if (section.toggle != null) {
+//                     section.toggle.setHoverDecorationColor(colors
+//                                     .getColor(IFormColors.TB_TOGGLE_HOVER));
+//                     section.toggle.setDecorationColor(colors
+//                                     .getColor(IFormColors.TB_TOGGLE));
+//             }
+//             section.setFont(boldFontHolder.getBoldFont(parent.getFont()));
+//             if ((sectionStyle & Section.TITLE_BAR) != 0
+//                             || (sectionStyle & Section.SHORT_TITLE_BAR) != 0) {
+//                     colors.initializeSectionToolBarColors();
+//                     section.setTitleBarBackground(colors.getColor(IFormColors.TB_BG));
+//                     section.setTitleBarBorderColor(colors
+//                                     .getColor(IFormColors.TB_BORDER));
+//             }
+//             // call setTitleBarForeground regardless as it also sets the label color
+//             section.setTitleBarForeground(colors
+//                             .getColor(IFormColors.TB_TOGGLE));
+//             return section;
+//     }
+
+       /**
+        * Creates an expandable composite as a part of the form.
+        *
+        * @param parent
+        *            the expandable composite parent
+        * @param expansionStyle
+        *            the expandable composite style
+        * @return the expandable composite widget
+        */
+//     public ExpandableComposite createExpandableComposite(Composite parent,
+//                     int expansionStyle) {
+//             ExpandableComposite ec = new ExpandableComposite(parent, orientation,
+//                             expansionStyle);
+//             ec.setMenu(parent.getMenu());
+//             adapt(ec, true, true);
+//             ec.setFont(boldFontHolder.getBoldFont(ec.getFont()));
+//             return ec;
+//     }
+
+       /**
+        * Creates a separator label as a part of the form.
+        *
+        * @param parent
+        *            the separator parent
+        * @param style
+        *            the separator style
+        * @return the separator label
+        */
+       public Label createSeparator(Composite parent, int style) {
+               Label label = new Label(parent, SWT.SEPARATOR | style | orientation);
+               label.setBackground(colors.getBackground());
+               label.setForeground(colors.getBorderColor());
+               return label;
+       }
+
+       /**
+        * Creates a table as a part of the form.
+        *
+        * @param parent
+        *            the table parent
+        * @param style
+        *            the table style
+        * @return the table widget
+        */
+       public Table createTable(Composite parent, int style) {
+               Table table = new Table(parent, style | borderStyle | orientation);
+               adapt(table, false, false);
+               // hookDeleteListener(table);
+               return table;
+       }
+
+       /**
+        * Creates a text as a part of the form.
+        *
+        * @param parent
+        *            the text parent
+        * @param value
+        *            the text initial value
+        * @return the text widget
+        */
+       public Text createText(Composite parent, String value) {
+               return createText(parent, value, SWT.SINGLE);
+       }
+
+       /**
+        * Creates a text as a part of the form.
+        *
+        * @param parent
+        *            the text parent
+        * @param value
+        *            the text initial value
+        * @param style
+        *            the text style
+        * @return the text widget
+        */
+       public Text createText(Composite parent, String value, int style) {
+               Text text = new Text(parent, borderStyle | style | orientation);
+               if (value != null)
+                       text.setText(value);
+               text.setForeground(colors.getForeground());
+               text.setBackground(colors.getBackground());
+               text.addFocusListener(visibilityHandler);
+               return text;
+       }
+
+       /**
+        * Creates a tree widget as a part of the form.
+        *
+        * @param parent
+        *            the tree parent
+        * @param style
+        *            the tree style
+        * @return the tree widget
+        */
+       public Tree createTree(Composite parent, int style) {
+               Tree tree = new Tree(parent, borderStyle | style | orientation);
+               adapt(tree, false, false);
+               // hookDeleteListener(tree);
+               return tree;
+       }
+
+       /**
+        * Creates a scrolled form widget in the provided parent. If you do not
+        * require scrolling because there is already a scrolled composite up the
+        * parent chain, use 'createForm' instead.
+        *
+        * @param parent
+        *            the scrolled form parent
+        * @return the form that can scroll itself
+        * @see #createForm
+        */
+       public ScrolledComposite createScrolledForm(Composite parent) {
+               ScrolledComposite form = new ScrolledComposite(parent, SWT.V_SCROLL
+                               | SWT.H_SCROLL | orientation);
+               form.setExpandHorizontal(true);
+               form.setExpandVertical(true);
+               form.setBackground(colors.getBackground());
+               form.setForeground(colors.getColor(IFormColors.TITLE));
+               form.setFont(JFaceResources.getHeaderFont());
+               return form;
+       }
+
+       /**
+        * Creates a form widget in the provided parent. Note that this widget does
+        * not scroll its content, so make sure there is a scrolled composite up the
+        * parent chain. If you require scrolling, use 'createScrolledForm' instead.
+        *
+        * @param parent
+        *            the form parent
+        * @return the form that does not scroll
+        * @see #createScrolledForm
+        */
+//     public Form createForm(Composite parent) {
+//             Form formContent = new Form(parent, orientation);
+//             formContent.setBackground(colors.getBackground());
+//             formContent.setForeground(colors.getColor(IFormColors.TITLE));
+//             formContent.setFont(JFaceResources.getHeaderFont());
+//             return formContent;
+//     }
+
+       /**
+        * Takes advantage of the gradients and other capabilities to decorate the
+        * form heading using colors computed based on the current skin and
+        * operating system.
+        *
+        * @param form
+        *            the form to decorate
+        */
+
+//     public void decorateFormHeading(Form form) {
+//             Color top = colors.getColor(IFormColors.H_GRADIENT_END);
+//             Color bot = colors.getColor(IFormColors.H_GRADIENT_START);
+//             form.setTextBackground(new Color[] { top, bot }, new int[] { 100 },
+//                             true);
+//             form.setHeadColor(IFormColors.H_BOTTOM_KEYLINE1, colors
+//                             .getColor(IFormColors.H_BOTTOM_KEYLINE1));
+//             form.setHeadColor(IFormColors.H_BOTTOM_KEYLINE2, colors
+//                             .getColor(IFormColors.H_BOTTOM_KEYLINE2));
+//             form.setHeadColor(IFormColors.H_HOVER_LIGHT, colors
+//                             .getColor(IFormColors.H_HOVER_LIGHT));
+//             form.setHeadColor(IFormColors.H_HOVER_FULL, colors
+//                             .getColor(IFormColors.H_HOVER_FULL));
+//             form.setHeadColor(IFormColors.TB_TOGGLE, colors
+//                             .getColor(IFormColors.TB_TOGGLE));
+//             form.setHeadColor(IFormColors.TB_TOGGLE_HOVER, colors
+//                             .getColor(IFormColors.TB_TOGGLE_HOVER));
+//             form.setSeparatorVisible(true);
+//     }
+
+       /**
+        * Creates a scrolled page book widget as a part of the form.
+        *
+        * @param parent
+        *            the page book parent
+        * @param style
+        *            the text style
+        * @return the scrolled page book widget
+        */
+//     public ScrolledPageBook createPageBook(Composite parent, int style) {
+//             ScrolledPageBook book = new ScrolledPageBook(parent, style
+//                             | orientation);
+//             adapt(book, true, true);
+//             book.setMenu(parent.getMenu());
+//             return book;
+//     }
+
+       /**
+        * Disposes the toolkit.
+        */
+       public void dispose() {
+               if (isDisposed) {
+                       return;
+               }
+               isDisposed = true;
+               if (colors.isShared() == false) {
+                       colors.dispose();
+                       colors = null;
+               }
+               boldFontHolder.dispose();
+       }
+
+       /**
+        * Returns the hyperlink group that manages hyperlinks for this toolkit.
+        *
+        * @return the hyperlink group
+        */
+//     public HyperlinkGroup getHyperlinkGroup() {
+//             return hyperlinkGroup;
+//     }
+
+       /**
+        * Sets the background color for the entire toolkit. The method delegates
+        * the call to the FormColors object and also updates the hyperlink group so
+        * that hyperlinks and other objects are in sync.
+        *
+        * @param bg
+        *            the new background color
+        */
+       public void setBackground(Color bg) {
+//             hyperlinkGroup.setBackground(bg);
+               colors.setBackground(bg);
+       }
+
+       /**
+        * Refreshes the hyperlink colors by loading from JFace settings.
+        */
+//     public void refreshHyperlinkColors() {
+//             hyperlinkGroup.initializeDefaultForegrounds(colors.getDisplay());
+//     }
+
+// RAP [rh] paintBordersFor not useful as no GC to actually paint borders
+//     /**
+//      * Paints flat borders for widgets created by this toolkit within the
+//      * provided parent. Borders will not be painted if the global border style
+//      * is SWT.BORDER (i.e. if native borders are used). Call this method during
+//      * creation of a form composite to get the borders of its children painted.
+//      * Care should be taken when selection layout margins. At least one pixel
+//      * pargin width and height must be chosen to allow the toolkit to paint the
+//      * border on the parent around the widgets.
+//      * <p>
+//      * Borders are painted for some controls that are selected by the toolkit by
+//      * default. If a control needs a border but is not on its list, it is
+//      * possible to force border in the following way:
+//      *
+//      * <pre>
+//      *
+//      *
+//      *
+//      *             widget.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TREE_BORDER);
+//      *
+//      *             or
+//      *
+//      *             widget.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TEXT_BORDER);
+//      *
+//      *
+//      *
+//      * </pre>
+//      *
+//      * @param parent
+//      *            the parent that owns the children for which the border needs
+//      *            to be painted.
+//      */
+//     public void paintBordersFor(Composite parent) {
+//             // if (borderStyle == SWT.BORDER)
+//             // return;
+//             if (borderPainter == null)
+//                     borderPainter = new BorderPainter();
+//             parent.addPaintListener(borderPainter);
+//     }
+
+       /**
+        * Returns the colors used by this toolkit.
+        *
+        * @return the color object
+        */
+       public FormColors getColors() {
+               return colors;
+       }
+
+       /**
+        * Returns the border style used for various widgets created by this
+        * toolkit. The intent of the toolkit is to create controls with styles that
+        * yield a 'flat' appearance. On systems where the native borders are
+        * already flat, we set the style to SWT.BORDER and don't paint the borders
+        * ourselves. Otherwise, the style is set to SWT.NULL, and borders are
+        * painted by the toolkit.
+        *
+        * @return the global border style
+        */
+       public int getBorderStyle() {
+               return borderStyle;
+       }
+
+       /**
+        * Returns the margin required around the children whose border is being
+        * painted by the toolkit using {@link #paintBordersFor(Composite)}. Since
+        * the border is painted around the controls on the parent, a number of
+        * pixels needs to be reserved for this border. For windowing systems where
+        * the native border is used, this margin is 0.
+        *
+        * @return the margin in the parent when children have their border painted
+        */
+       public int getBorderMargin() {
+               return getBorderStyle() == SWT.BORDER ? 0 : 2;
+       }
+
+       /**
+        * Sets the border style to be used when creating widgets. The toolkit
+        * chooses the correct style based on the platform but this value can be
+        * changed using this method.
+        *
+        * @param style
+        *            <code>SWT.BORDER</code> or <code>SWT.NULL</code>
+        * @see #getBorderStyle
+        */
+       public void setBorderStyle(int style) {
+               this.borderStyle = style;
+       }
+
+       /**
+        * A utility method that ensures that the control is visible in the scrolled
+        * composite. The prerequisite for this method is that the control has a
+        * class that extends ScrolledComposite somewhere in the parent chain. If
+        * the control is partially or fully clipped, the composite is scrolled to
+        * set by setting the origin to the control origin.
+        *
+        * @param c
+        *            the control to make visible
+        * @param verticalOnly
+        *            if <code>true</code>, the scrolled composite will be
+        *            scrolled only vertically if needed. Otherwise, the scrolled
+        *            composite origin will be set to the control origin.
+        */
+       public static void setControlVisible(Control c, boolean verticalOnly) {
+               ScrolledComposite scomp = FormUtil.getScrolledComposite(c);
+               if (scomp == null)
+                       return;
+               Point location = FormUtil.getControlLocation(scomp, c);
+               scomp.setOrigin(location);
+       }
+
+       private void initialize() {
+               initializeBorderStyle();
+//             hyperlinkGroup = new HyperlinkGroup(colors.getDisplay());
+//             hyperlinkGroup.setBackground(colors.getBackground());
+               visibilityHandler = new VisibilityHandler();
+               keyboardHandler = new KeyboardHandler();
+               boldFontHolder = new BoldFontHolder();
+       }
+
+// RAP [rh] revise detection of border style: can't ask OS here
+       private void initializeBorderStyle() {
+//             String osname = System.getProperty("os.name"); //$NON-NLS-1$
+//             String osversion = System.getProperty("os.version"); //$NON-NLS-1$
+//             if (osname.startsWith("Windows") && "5.1".compareTo(osversion) <= 0) { //$NON-NLS-1$ //$NON-NLS-2$
+//                     // Skinned widgets used on newer Windows (e.g. XP (5.1), Vista
+//                     // (6.0))
+//                     // Check for Windows Classic. If not used, set the style to BORDER
+//                     RGB rgb = colors.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
+//                     if (rgb.red != 212 || rgb.green != 208 || rgb.blue != 200)
+//                             borderStyle = SWT.BORDER;
+//             } else if (osname.startsWith("Mac")) //$NON-NLS-1$
+//                     borderStyle = SWT.BORDER;
+
+               borderStyle = SWT.BORDER;
+       }
+
+       /**
+        * Returns the orientation that all the widgets created by this toolkit will
+        * inherit, if set. Can be <code>SWT.NULL</code>,
+        * <code>SWT.LEFT_TO_RIGHT</code> and <code>SWT.RIGHT_TO_LEFT</code>.
+        *
+        * @return orientation style for this toolkit, or <code>SWT.NULL</code> if
+        *         not set. The default orientation is inherited from the Window
+        *         default orientation.
+        * @see org.eclipse.jface.window.Window#getDefaultOrientation()
+        */
+
+       public int getOrientation() {
+               return orientation;
+       }
+
+       /**
+        * Sets the orientation that all the widgets created by this toolkit will
+        * inherit. Can be <code>SWT.NULL</code>, <code>SWT.LEFT_TO_RIGHT</code>
+        * and <code>SWT.RIGHT_TO_LEFT</code>.
+        *
+        * @param orientation
+        *            style for this toolkit.
+        */
+
+       public void setOrientation(int orientation) {
+               this.orientation = orientation;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormUtil.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormUtil.java
new file mode 100644 (file)
index 0000000..76e3f11
--- /dev/null
@@ -0,0 +1,522 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.MouseEvent;
+//import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.FontMetrics;
+import org.eclipse.swt.graphics.GC;
+//import org.eclipse.swt.graphics.Image;
+//import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Layout;
+//import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.Text;
+//import org.eclipse.ui.forms.widgets.ColumnLayout;
+//import org.eclipse.ui.forms.widgets.Form;
+//import org.eclipse.ui.forms.widgets.FormText;
+//import org.eclipse.ui.forms.widgets.FormToolkit;
+//import org.eclipse.ui.forms.widgets.ILayoutExtension;
+//
+//import com.ibm.icu.text.BreakIterator;
+
+public class FormUtil {
+       public static final String PLUGIN_ID = "org.eclipse.ui.forms"; //$NON-NLS-1$
+
+       static final int H_SCROLL_INCREMENT = 5;
+
+       static final int V_SCROLL_INCREMENT = 64;
+
+       public static final String DEBUG = PLUGIN_ID + "/debug"; //$NON-NLS-1$
+
+       public static final String DEBUG_TEXT = DEBUG + "/text"; //$NON-NLS-1$
+       public static final String DEBUG_TEXTSIZE = DEBUG + "/textsize"; //$NON-NLS-1$
+
+       public static final String DEBUG_FOCUS = DEBUG + "/focus"; //$NON-NLS-1$
+
+       public static final String FOCUS_SCROLLING = "focusScrolling"; //$NON-NLS-1$
+       
+       public static final String IGNORE_BODY = "__ignore_body__"; //$NON-NLS-1$
+
+       public static Text createText(Composite parent, String label,
+                       FormToolkit factory) {
+               return createText(parent, label, factory, 1);
+       }
+
+       public static Text createText(Composite parent, String label,
+                       FormToolkit factory, int span) {
+               factory.createLabel(parent, label);
+               Text text = factory.createText(parent, ""); //$NON-NLS-1$
+               int hfill = span == 1 ? GridData.FILL_HORIZONTAL
+                               : GridData.HORIZONTAL_ALIGN_FILL;
+               GridData gd = new GridData(hfill | GridData.VERTICAL_ALIGN_CENTER);
+               gd.horizontalSpan = span;
+               text.setLayoutData(gd);
+               return text;
+       }
+
+       public static Text createText(Composite parent, String label,
+                       FormToolkit factory, int span, int style) {
+               Label l = factory.createLabel(parent, label);
+               if ((style & SWT.MULTI) != 0) {
+                       GridData gd = new GridData(GridData.VERTICAL_ALIGN_BEGINNING);
+                       l.setLayoutData(gd);
+               }
+               Text text = factory.createText(parent, "", style); //$NON-NLS-1$
+               int hfill = span == 1 ? GridData.FILL_HORIZONTAL
+                               : GridData.HORIZONTAL_ALIGN_FILL;
+               GridData gd = new GridData(hfill | GridData.VERTICAL_ALIGN_CENTER);
+               gd.horizontalSpan = span;
+               text.setLayoutData(gd);
+               return text;
+       }
+
+       public static Text createText(Composite parent, FormToolkit factory,
+                       int span) {
+               Text text = factory.createText(parent, ""); //$NON-NLS-1$
+               int hfill = span == 1 ? GridData.FILL_HORIZONTAL
+                               : GridData.HORIZONTAL_ALIGN_FILL;
+               GridData gd = new GridData(hfill | GridData.VERTICAL_ALIGN_CENTER);
+               gd.horizontalSpan = span;
+               text.setLayoutData(gd);
+               return text;
+       }
+
+       public static int computeMinimumWidth(GC gc, String text) {
+//             BreakIterator wb = BreakIterator.getWordInstance();
+//             wb.setText(text);
+//             int last = 0;
+//
+//             int width = 0;
+//
+//             for (int loc = wb.first(); loc != BreakIterator.DONE; loc = wb.next()) {
+//                     String word = text.substring(last, loc);
+//                     Point extent = gc.textExtent(word);
+//                     width = Math.max(width, extent.x);
+//                     last = loc;
+//             }
+//             String lastWord = text.substring(last);
+//             Point extent = gc.textExtent(lastWord);
+//             width = Math.max(width, extent.x);
+//             return width;
+               return 0;
+       }
+       
+       public static Point computeWrapSize(GC gc, String text, int wHint) {    
+//             BreakIterator wb = BreakIterator.getWordInstance();
+//             wb.setText(text);
+               FontMetrics fm = gc.getFontMetrics();
+               int lineHeight = fm.getHeight();
+               
+               int saved = 0;
+               int last = 0;
+               int height = lineHeight;
+               int maxWidth = 0;
+//             for (int loc = wb.first(); loc != BreakIterator.DONE; loc = wb.next()) {
+//                     String word = text.substring(saved, loc);
+//                     Point extent = gc.textExtent(word);
+//                     if (extent.x > wHint) {
+//                             // overflow
+//                             saved = last;
+//                             height += extent.y;
+//                             // switch to current word so maxWidth will accommodate very long single words
+//                             word = text.substring(last, loc);
+//                             extent = gc.textExtent(word);
+//                     }
+//                     maxWidth = Math.max(maxWidth, extent.x);
+//                     last = loc;
+//             }
+               /*
+                * Correct the height attribute in case it was calculated wrong due to wHint being less than maxWidth.
+                * The recursive call proved to be the only thing that worked in all cases. Some attempts can be made
+                * to estimate the height, but the algorithm needs to be run again to be sure.
+                */
+               if (maxWidth > wHint)
+                       return computeWrapSize(gc, text, maxWidth);               
+               return new Point(maxWidth, height);
+       }
+
+// RAP [rh] paintWrapText unnecessary
+//     public static void paintWrapText(GC gc, String text, Rectangle bounds) {
+//             paintWrapText(gc, text, bounds, false);
+//     }
+       
+// RAP [rh] paintWrapText unnecessary
+//     public static void paintWrapText(GC gc, String text, Rectangle bounds,
+//                     boolean underline) {
+//             BreakIterator wb = BreakIterator.getWordInstance();
+//             wb.setText(text);
+//             FontMetrics fm = gc.getFontMetrics();
+//             int lineHeight = fm.getHeight();
+//             int descent = fm.getDescent();
+//
+//             int saved = 0;
+//             int last = 0;
+//             int y = bounds.y;
+//             int width = bounds.width;
+//
+//             for (int loc = wb.first(); loc != BreakIterator.DONE; loc = wb.next()) {
+//                     String line = text.substring(saved, loc);
+//                     Point extent = gc.textExtent(line);
+//
+//                     if (extent.x > width) {
+//                             // overflow
+//                             String prevLine = text.substring(saved, last);
+//                             gc.drawText(prevLine, bounds.x, y, true);
+//                             if (underline) {
+//                                     Point prevExtent = gc.textExtent(prevLine);
+//                                     int lineY = y + lineHeight - descent + 1;
+//                                     gc
+//                                                     .drawLine(bounds.x, lineY, bounds.x + prevExtent.x,
+//                                                                     lineY);
+//                             }
+//
+//                             saved = last;
+//                             y += lineHeight;
+//                     }
+//                     last = loc;
+//             }
+//             // paint the last line
+//             String lastLine = text.substring(saved, last);
+//             gc.drawText(lastLine, bounds.x, y, true);
+//             if (underline) {
+//                     int lineY = y + lineHeight - descent + 1;
+//                     Point lastExtent = gc.textExtent(lastLine);
+//                     gc.drawLine(bounds.x, lineY, bounds.x + lastExtent.x, lineY);
+//             }
+//     }
+
+       public static ScrolledComposite getScrolledComposite(Control c) {
+               Composite parent = c.getParent();
+
+               while (parent != null) {
+                       if (parent instanceof ScrolledComposite) {
+                               return (ScrolledComposite) parent;
+                       }
+                       parent = parent.getParent();
+               }
+               return null;
+       }
+
+       public static void ensureVisible(Control c) {
+               ScrolledComposite scomp = getScrolledComposite(c);
+               if (scomp != null) {
+                       Object data = scomp.getData(FOCUS_SCROLLING);
+                       if (data == null || !data.equals(Boolean.FALSE))
+                               FormUtil.ensureVisible(scomp, c);
+               }
+       }
+
+       public static void ensureVisible(ScrolledComposite scomp, Control control) {
+               // if the control is a FormText we do not need to scroll since it will
+               // ensure visibility of its segments as necessary
+//             if (control instanceof FormText)
+//                     return;
+               Point controlSize = control.getSize();
+               Point controlOrigin = getControlLocation(scomp, control);
+               ensureVisible(scomp, controlOrigin, controlSize);
+       }
+
+       public static void ensureVisible(ScrolledComposite scomp,
+                       Point controlOrigin, Point controlSize) {
+               Rectangle area = scomp.getClientArea();
+               Point scompOrigin = scomp.getOrigin();
+
+               int x = scompOrigin.x;
+               int y = scompOrigin.y;
+
+               // horizontal right, but only if the control is smaller
+               // than the client area
+               if (controlSize.x < area.width
+                               && (controlOrigin.x + controlSize.x > scompOrigin.x
+                                               + area.width)) {
+                       x = controlOrigin.x + controlSize.x - area.width;
+               }
+               // horizontal left - make sure the left edge of
+               // the control is showing
+               if (controlOrigin.x < x) {
+                       if (controlSize.x < area.width)
+                               x = controlOrigin.x + controlSize.x - area.width;
+                       else
+                               x = controlOrigin.x;
+               }
+               // vertical bottom
+               if (controlSize.y < area.height
+                               && (controlOrigin.y + controlSize.y > scompOrigin.y
+                                               + area.height)) {
+                       y = controlOrigin.y + controlSize.y - area.height;
+               }
+               // vertical top - make sure the top of
+               // the control is showing
+               if (controlOrigin.y < y) {
+                       if (controlSize.y < area.height)
+                               y = controlOrigin.y + controlSize.y - area.height;
+                       else
+                               y = controlOrigin.y;
+               }
+
+               if (scompOrigin.x != x || scompOrigin.y != y) {
+                       // scroll to reveal
+                       scomp.setOrigin(x, y);
+               }
+       }
+
+       public static void ensureVisible(ScrolledComposite scomp, Control control,
+                       MouseEvent e) {
+               Point controlOrigin = getControlLocation(scomp, control);
+               int rX = controlOrigin.x + e.x;
+               int rY = controlOrigin.y + e.y;
+               Rectangle area = scomp.getClientArea();
+               Point scompOrigin = scomp.getOrigin();
+
+               int x = scompOrigin.x;
+               int y = scompOrigin.y;
+               // System.out.println("Ensure: area="+area+", origin="+scompOrigin+",
+               // cloc="+controlOrigin+", csize="+controlSize+", x="+x+", y="+y);
+
+               // horizontal right
+               if (rX > scompOrigin.x + area.width) {
+                       x = rX - area.width;
+               }
+               // horizontal left
+               else if (rX < x) {
+                       x = rX;
+               }
+               // vertical bottom
+               if (rY > scompOrigin.y + area.height) {
+                       y = rY - area.height;
+               }
+               // vertical top
+               else if (rY < y) {
+                       y = rY;
+               }
+
+               if (scompOrigin.x != x || scompOrigin.y != y) {
+                       // scroll to reveal
+                       scomp.setOrigin(x, y);
+               }
+       }
+
+       public static Point getControlLocation(ScrolledComposite scomp,
+                       Control control) {
+               int x = 0;
+               int y = 0;
+               Control content = scomp.getContent();
+               Control currentControl = control;
+               for (;;) {
+                       if (currentControl == content)
+                               break;
+                       Point location = currentControl.getLocation();
+                       // if (location.x > 0)
+                       // x += location.x;
+                       // if (location.y > 0)
+                       // y += location.y;
+                       x += location.x;
+                       y += location.y;
+                       currentControl = currentControl.getParent();
+               }
+               return new Point(x, y);
+       }
+
+       static void scrollVertical(ScrolledComposite scomp, boolean up) {
+               scroll(scomp, 0, up ? -V_SCROLL_INCREMENT : V_SCROLL_INCREMENT);
+       }
+
+       static void scrollHorizontal(ScrolledComposite scomp, boolean left) {
+               scroll(scomp, left ? -H_SCROLL_INCREMENT : H_SCROLL_INCREMENT, 0);
+       }
+
+       static void scrollPage(ScrolledComposite scomp, boolean up) {
+               Rectangle clientArea = scomp.getClientArea();
+               int increment = up ? -clientArea.height : clientArea.height;
+               scroll(scomp, 0, increment);
+       }
+
+       static void scroll(ScrolledComposite scomp, int xoffset, int yoffset) {
+               Point origin = scomp.getOrigin();
+               Point contentSize = scomp.getContent().getSize();
+               int xorigin = origin.x + xoffset;
+               int yorigin = origin.y + yoffset;
+               xorigin = Math.max(xorigin, 0);
+               xorigin = Math.min(xorigin, contentSize.x - 1);
+               yorigin = Math.max(yorigin, 0);
+               yorigin = Math.min(yorigin, contentSize.y - 1);
+               scomp.setOrigin(xorigin, yorigin);
+       }
+
+// RAP [rh] FormUtil#updatePageIncrement: empty implementation
+       public static void updatePageIncrement(ScrolledComposite scomp) {
+//             ScrollBar vbar = scomp.getVerticalBar();
+//             if (vbar != null) {
+//                     Rectangle clientArea = scomp.getClientArea();
+//                     int increment = clientArea.height - 5;
+//                     vbar.setPageIncrement(increment);
+//             }
+//             ScrollBar hbar = scomp.getHorizontalBar();
+//             if (hbar != null) {
+//                     Rectangle clientArea = scomp.getClientArea();
+//                     int increment = clientArea.width - 5;
+//                     hbar.setPageIncrement(increment);
+//             }
+       }
+
+       public static void processKey(int keyCode, Control c) {
+               if (c.isDisposed()) {
+                       return;
+               }
+               ScrolledComposite scomp = FormUtil.getScrolledComposite(c);
+               if (scomp != null) {
+                       if (c instanceof Combo)
+                               return;
+                       switch (keyCode) {
+                       case SWT.ARROW_DOWN:
+                               if (scomp.getData("novarrows") == null) //$NON-NLS-1$
+                                       FormUtil.scrollVertical(scomp, false);
+                               break;
+                       case SWT.ARROW_UP:
+                               if (scomp.getData("novarrows") == null) //$NON-NLS-1$
+                                       FormUtil.scrollVertical(scomp, true);
+                               break;
+                       case SWT.ARROW_LEFT:
+                               FormUtil.scrollHorizontal(scomp, true);
+                               break;
+                       case SWT.ARROW_RIGHT:
+                               FormUtil.scrollHorizontal(scomp, false);
+                               break;
+                       case SWT.PAGE_UP:
+                               FormUtil.scrollPage(scomp, true);
+                               break;
+                       case SWT.PAGE_DOWN:
+                               FormUtil.scrollPage(scomp, false);
+                               break;
+                       }
+               }
+       }
+
+       public static boolean isWrapControl(Control c) {
+               if ((c.getStyle() & SWT.WRAP) != 0)
+                       return true;
+               if (c instanceof Composite) {
+                       return false;
+//                     return ((Composite) c).getLayout() instanceof ILayoutExtension;
+               }
+               return false;
+       }
+
+       public static int getWidthHint(int wHint, Control c) {
+               boolean wrap = isWrapControl(c);
+               return wrap ? wHint : SWT.DEFAULT;
+       }
+
+       public static int getHeightHint(int hHint, Control c) {
+               if (c instanceof Composite) {
+                       Layout layout = ((Composite) c).getLayout();
+//                     if (layout instanceof ColumnLayout)
+//                             return hHint;
+               }
+               return SWT.DEFAULT;
+       }
+
+       public static int computeMinimumWidth(Control c, boolean changed) {
+               if (c instanceof Composite) {
+                       Layout layout = ((Composite) c).getLayout();
+//                     if (layout instanceof ILayoutExtension)
+//                             return ((ILayoutExtension) layout).computeMinimumWidth(
+//                                             (Composite) c, changed);
+               }
+               return c.computeSize(FormUtil.getWidthHint(5, c), SWT.DEFAULT, changed).x;
+       }
+
+       public static int computeMaximumWidth(Control c, boolean changed) {
+               if (c instanceof Composite) {
+                       Layout layout = ((Composite) c).getLayout();
+//                     if (layout instanceof ILayoutExtension)
+//                             return ((ILayoutExtension) layout).computeMaximumWidth(
+//                                             (Composite) c, changed);
+               }
+               return c.computeSize(SWT.DEFAULT, SWT.DEFAULT, changed).x;
+       }
+
+//     public static Form getForm(Control c) {
+//             Composite parent = c.getParent();
+//             while (parent != null) {
+//                     if (parent instanceof Form) {
+//                             return (Form) parent;
+//                     }
+//                     parent = parent.getParent();
+//             }
+//             return null;
+//     }
+
+// RAP [rh] FormUtil#createAlphaMashImage unnecessary  
+//     public static Image createAlphaMashImage(Device device, Image srcImage) {
+//             Rectangle bounds = srcImage.getBounds();
+//             int alpha = 0;
+//             int calpha = 0;
+//             ImageData data = srcImage.getImageData();
+//             // Create a new image with alpha values alternating
+//             // between fully transparent (0) and fully opaque (255).
+//             // This image will show the background through the
+//             // transparent pixels.
+//             for (int i = 0; i < bounds.height; i++) {
+//                     // scan line
+//                     alpha = calpha;
+//                     for (int j = 0; j < bounds.width; j++) {
+//                             // column
+//                             data.setAlpha(j, i, alpha);
+//                             alpha = alpha == 255 ? 0 : 255;
+//                     }
+//                     calpha = calpha == 255 ? 0 : 255;
+//             }
+//             return new Image(device, data);
+//     }
+
+       public static boolean mnemonicMatch(String text, char key) {
+               char mnemonic = findMnemonic(text);
+               if (mnemonic == '\0')
+                       return false;
+               return Character.toUpperCase(key) == Character.toUpperCase(mnemonic);
+       }
+
+       private static char findMnemonic(String string) {
+               int index = 0;
+               int length = string.length();
+               do {
+                       while (index < length && string.charAt(index) != '&')
+                               index++;
+                       if (++index >= length)
+                               return '\0';
+                       if (string.charAt(index) != '&')
+                               return string.charAt(index);
+                       index++;
+               } while (index < length);
+               return '\0';
+       }
+       
+       public static void setFocusScrollingEnabled(Control c, boolean enabled) {
+               ScrolledComposite scomp = null;
+               
+               if (c instanceof ScrolledComposite)
+                       scomp = (ScrolledComposite)c;
+               else
+                       scomp = getScrolledComposite(c);
+               if (scomp!=null)
+                       scomp.setData(FormUtil.FOCUS_SCROLLING, enabled?null:Boolean.FALSE);
+       }
+       
+       // RAP [rh] FormUtil#setAntialias unnecessary
+//     public static void setAntialias(GC gc, int style) {
+//             if (!gc.getAdvanced()) {
+//                     gc.setAdvanced(true);
+//                     if (!gc.getAdvanced())
+//                             return;
+//             }
+//             gc.setAntialias(style);
+//     }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IFormColors.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IFormColors.java
new file mode 100644 (file)
index 0000000..cf0e5d3
--- /dev/null
@@ -0,0 +1,102 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+/**
+ * A place to hold all the color constants used in the forms package.
+ * 
+ * @since 1.0
+ */
+
+public interface IFormColors {
+       /**
+        * A prefix for all the keys.
+        */
+       String PREFIX = "org.eclipse.ui.forms."; //$NON-NLS-1$
+       /**
+        * Key for the form title foreground color.
+        */
+       String TITLE = PREFIX + "TITLE"; //$NON-NLS-1$
+
+       /**
+        * A prefix for the header color constants.
+        */
+       String H_PREFIX = PREFIX + "H_"; //$NON-NLS-1$
+       /*
+        * A prefix for the section title bar color constants.
+        */
+       String TB_PREFIX = PREFIX + "TB_"; //$NON-NLS-1$        
+       /**
+        * Key for the form header background gradient ending color.
+        */
+       String H_GRADIENT_END = H_PREFIX + "GRADIENT_END"; //$NON-NLS-1$
+
+       /**
+        * Key for the form header background gradient starting color.
+        * 
+        */
+       String H_GRADIENT_START = H_PREFIX + "GRADIENT_START"; //$NON-NLS-1$
+       /**
+        * Key for the form header bottom keyline 1 color.
+        * 
+        */
+       String H_BOTTOM_KEYLINE1 = H_PREFIX + "BOTTOM_KEYLINE1"; //$NON-NLS-1$
+       /**
+        * Key for the form header bottom keyline 2 color.
+        * 
+        */
+       String H_BOTTOM_KEYLINE2 = H_PREFIX + "BOTTOM_KEYLINE2"; //$NON-NLS-1$
+       /**
+        * Key for the form header light hover color.
+        * 
+        */
+       String H_HOVER_LIGHT = H_PREFIX + "H_HOVER_LIGHT"; //$NON-NLS-1$
+       /**
+        * Key for the form header full hover color.
+        * 
+        */
+       String H_HOVER_FULL = H_PREFIX + "H_HOVER_FULL"; //$NON-NLS-1$
+
+       /**
+        * Key for the tree/table border color.
+        */
+       String BORDER = PREFIX + "BORDER"; //$NON-NLS-1$
+
+       /**
+        * Key for the section separator color.
+        */
+       String SEPARATOR = PREFIX + "SEPARATOR"; //$NON-NLS-1$
+
+       /**
+        * Key for the section title bar background.
+        */
+       String TB_BG = TB_PREFIX + "BG"; //$NON-NLS-1$
+
+       /**
+        * Key for the section title bar foreground.
+        */
+       String TB_FG = TB_PREFIX + "FG"; //$NON-NLS-1$
+
+       /**
+        * Key for the section title bar gradient.
+        * @deprecated Since 3.3, this color is not used any more. The 
+        * tool bar gradient is created starting from {@link #TB_BG} to
+        * the section background color.
+        */
+       String TB_GBG = TB_BG;
+
+       /**
+        * Key for the section title bar border.
+        */
+       String TB_BORDER = TB_PREFIX + "BORDER"; //$NON-NLS-1$
+
+       /**
+        * Key for the section toggle color. Since 3.1, this color is used for all
+        * section styles.
+        */
+       String TB_TOGGLE = TB_PREFIX + "TOGGLE"; //$NON-NLS-1$
+
+       /**
+        * Key for the section toggle hover color.
+        * 
+        */
+       String TB_TOGGLE_HOVER = TB_PREFIX + "TOGGLE_HOVER"; //$NON-NLS-1$              
+}
\ No newline at end of file
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IFormPart.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IFormPart.java
new file mode 100644 (file)
index 0000000..954cc03
--- /dev/null
@@ -0,0 +1,108 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+/**
+ * Classes that implement this interface can be added to the managed form and
+ * take part in the form life cycle. The part is initialized with the form and
+ * will be asked to accept focus. The part can receive form input and can elect
+ * to do something according to it (for example, select an object that matches
+ * the input).
+ * <p>
+ * The form part has two 'out of sync' states in respect to the model(s) that
+ * feed the form: <b>dirty</b> and <b>stale</b>. When a part is dirty, it
+ * means that the user interacted with it and now its widgets contain state that
+ * is newer than the model. In order to sync up with the model, 'commit' needs
+ * to be called. In contrast, the model can change 'under' the form (as a result
+ * of some actions outside the form), resulting in data in the model being
+ * 'newer' than the content presented in the form. A 'stale' form part is
+ * brought in sync with the model by calling 'refresh'. The part is responsible
+ * for notifying the form when one of these states change in the part. The form
+ * reserves the right to handle this notification in the most appropriate way
+ * for the situation (for example, if the form is in a page of the multi-page
+ * editor, it may do nothing for stale parts if the page is currently not
+ * showing).
+ * <p>
+ * When the form is disposed, each registered part is disposed as well. Parts
+ * are responsible for releasing any system resources they created and for
+ * removing themselves as listeners from all event providers.
+ * 
+ * @see IManagedForm
+ * @since 1.0
+ * 
+ */
+public interface IFormPart {
+       /**
+        * Initializes the part.
+        * 
+        * @param form
+        *            the managed form that manages the part
+        */
+       void initialize(IManagedForm form);
+
+       /**
+        * Disposes the part allowing it to release allocated resources.
+        */
+       void dispose();
+
+       /**
+        * Returns true if the part has been modified with respect to the data
+        * loaded from the model.
+        * 
+        * @return true if the part has been modified with respect to the data
+        *         loaded from the model
+        */
+       boolean isDirty();
+
+       /**
+        * If part is displaying information loaded from a model, this method
+        * instructs it to commit the new (modified) data back into the model.
+        * 
+        * @param onSave
+        *            indicates if commit is called during 'save' operation or for
+        *            some other reason (for example, if form is contained in a
+        *            wizard or a multi-page editor and the user is about to leave
+        *            the page).
+        */
+       void commit(boolean onSave);
+
+       /**
+        * Notifies the part that an object has been set as overall form's input.
+        * The part can elect to react by revealing or selecting the object, or do
+        * nothing if not applicable.
+        * 
+        * @return <code>true</code> if the part has selected and revealed the
+        *         input object, <code>false</code> otherwise.
+        */
+       boolean setFormInput(Object input);
+
+       /**
+        * Instructs form part to transfer focus to the widget that should has focus
+        * in that part. The method can do nothing (if it has no widgets capable of
+        * accepting focus).
+        */
+       void setFocus();
+
+       /**
+        * Tests whether the form part is stale and needs refreshing. Parts can
+        * receive notification from models that will make their content stale, but
+        * may need to delay refreshing to improve performance (for example, there
+        * is no need to immediately refresh a part on a form that is current on a
+        * hidden page).
+        * <p>
+        * It is important to differentiate 'stale' and 'dirty' states. Part is
+        * 'dirty' if user interacted with its editable widgets and changed the
+        * values. In contrast, part is 'stale' when the data it presents in the
+        * widgets has been changed in the model without direct user interaction.
+        * 
+        * @return <code>true</code> if the part needs refreshing,
+        *         <code>false</code> otherwise.
+        */
+       boolean isStale();
+
+       /**
+        * Refreshes the part completely from the information freshly obtained from
+        * the model. The method will not be called if the part is not stale.
+        * Otherwise, the part is responsible for clearing the 'stale' flag after
+        * refreshing itself.
+        */
+       void refresh();
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IManagedForm.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IManagedForm.java
new file mode 100644 (file)
index 0000000..490d3a3
--- /dev/null
@@ -0,0 +1,175 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.swt.custom.ScrolledComposite;
+//import org.eclipse.ui.forms.widgets.FormToolkit;
+//import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * Managed form wraps a form widget and adds life cycle methods for form parts.
+ * A form part is a portion of the form that participates in form life cycle
+ * events.
+ * <p>
+ * There is no 1/1 mapping between widgets and form parts. A widget like Section
+ * can be a part by itself, but a number of widgets can gather around one form
+ * part.
+ * <p>
+ * This interface should not be extended or implemented. New form instances
+ * should be created using ManagedForm.
+ * 
+ * @see ManagedForm
+ * @since 1.0
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface IManagedForm {
+       /**
+        * Initializes the form by looping through the managed parts and
+        * initializing them. Has no effect if already called once.
+        */
+       public void initialize();
+
+       /**
+        * Returns the toolkit used by this form.
+        * 
+        * @return the toolkit
+        */
+       public FormToolkit getToolkit();
+
+       /**
+        * Returns the form widget managed by this form.
+        * 
+        * @return the form widget
+        */
+       public ScrolledComposite getForm();
+
+       /**
+        * Reflows the form as a result of the layout change.
+        * 
+        * @param changed
+        *            if <code>true</code>, discard cached layout information
+        */
+       public void reflow(boolean changed);
+
+       /**
+        * A part can use this method to notify other parts that implement
+        * IPartSelectionListener about selection changes.
+        * 
+        * @param part
+        *            the part that broadcasts the selection
+        * @param selection
+        *            the selection in the part
+        */
+       public void fireSelectionChanged(IFormPart part, ISelection selection);
+
+       /**
+        * Returns all the parts currently managed by this form.
+        * 
+        * @return the managed parts
+        */
+       IFormPart[] getParts();
+
+       /**
+        * Adds the new part to the form.
+        * 
+        * @param part
+        *            the part to add
+        */
+       void addPart(IFormPart part);
+
+       /**
+        * Removes the part from the form.
+        * 
+        * @param part
+        *            the part to remove
+        */
+       void removePart(IFormPart part);
+
+       /**
+        * Sets the input of this page to the provided object.
+        * 
+        * @param input
+        *            the new page input
+        * @return <code>true</code> if the form contains this object,
+        *         <code>false</code> otherwise.
+        */
+       boolean setInput(Object input);
+
+       /**
+        * Returns the current page input.
+        * 
+        * @return page input object or <code>null</code> if not applicable.
+        */
+       Object getInput();
+
+       /**
+        * Tests if form is dirty. A managed form is dirty if at least one managed
+        * part is dirty.
+        * 
+        * @return <code>true</code> if at least one managed part is dirty,
+        *         <code>false</code> otherwise.
+        */
+       boolean isDirty();
+
+       /**
+        * Notifies the form that the dirty state of one of its parts has changed.
+        * The global dirty state of the form can be obtained by calling 'isDirty'.
+        * 
+        * @see #isDirty
+        */
+       void dirtyStateChanged();
+
+       /**
+        * Commits the dirty form. All pending changes in the widgets are flushed
+        * into the model.
+        * 
+        * @param onSave
+        */
+       void commit(boolean onSave);
+
+       /**
+        * Tests if form is stale. A managed form is stale if at least one managed
+        * part is stale. This can happen when the underlying model changes,
+        * resulting in the presentation of the part being out of sync with the
+        * model and needing refreshing.
+        * 
+        * @return <code>true</code> if the form is stale, <code>false</code>
+        *         otherwise.
+        */
+       boolean isStale();
+
+       /**
+        * Notifies the form that the stale state of one of its parts has changed.
+        * The global stale state of the form can be obtained by calling 'isStale'.
+        */
+       void staleStateChanged();
+
+       /**
+        * Refreshes the form by refreshing every part that is stale.
+        */
+       void refresh();
+
+       /**
+        * Sets the container that owns this form. Depending on the context, the
+        * container may be wizard, editor page, editor etc.
+        * 
+        * @param container
+        *            the container of this form
+        */
+       void setContainer(Object container);
+
+       /**
+        * Returns the container of this form.
+        * 
+        * @return the form container
+        */
+       Object getContainer();
+
+       /**
+        * Returns the message manager that will keep track of messages in this
+        * form.
+        * 
+        * @return the message manager instance
+        */
+//     IMessageManager getMessageManager();
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IPartSelectionListener.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IPartSelectionListener.java
new file mode 100644 (file)
index 0000000..0f557d4
--- /dev/null
@@ -0,0 +1,23 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+import org.eclipse.jface.viewers.ISelection;
+
+/**
+ * Form parts can implement this interface if they want to be 
+ * notified when another part on the same form changes selection 
+ * state.
+ * 
+ * @see IFormPart
+ * @since 1.0
+ */
+public interface IPartSelectionListener {
+       /**
+        * Called when the provided part has changed selection state.
+        * 
+        * @param part
+        *            the selection source
+        * @param selection
+        *            the new selection
+        */
+       public void selectionChanged(IFormPart part, ISelection selection);
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/ManagedForm.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/ManagedForm.java
new file mode 100644 (file)
index 0000000..4140465
--- /dev/null
@@ -0,0 +1,323 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+import java.util.Vector;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.widgets.Composite;
+//import org.eclipse.ui.forms.widgets.FormToolkit;
+//import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * Managed form wraps a form widget and adds life cycle methods for form parts.
+ * A form part is a portion of the form that participates in form life cycle
+ * events.
+ * <p>
+ * There is requirement for 1/1 mapping between widgets and form parts. A widget
+ * like Section can be a part by itself, but a number of widgets can join around
+ * one form part.
+ * <p>
+ * Note to developers: this class is left public to allow its use beyond the
+ * original intention (inside a multi-page editor's page). You should limit the
+ * use of this class to make new instances inside a form container (wizard page,
+ * dialog etc.). Clients that need access to the class should not do it
+ * directly. Instead, they should do it through IManagedForm interface as much
+ * as possible.
+ * 
+ * @since 1.0
+ */
+public class ManagedForm implements IManagedForm {
+       private Object input;
+
+       private ScrolledComposite form;
+
+       private FormToolkit toolkit;
+
+       private Object container;
+
+       private boolean ownsToolkit;
+
+       private boolean initialized;
+
+       private Vector parts = new Vector();
+
+       /**
+        * Creates a managed form in the provided parent. Form toolkit and widget
+        * will be created and owned by this object.
+        * 
+        * @param parent
+        *            the parent widget
+        */
+       public ManagedForm(Composite parent) {
+               toolkit = new FormToolkit(parent.getDisplay());
+               ownsToolkit = true;
+               form = toolkit.createScrolledForm(parent);
+               
+       }
+
+       /**
+        * Creates a managed form that will use the provided toolkit and
+        * 
+        * @param toolkit
+        * @param form
+        */
+       public ManagedForm(FormToolkit toolkit, ScrolledComposite form) {
+               this.form = form;
+               this.toolkit = toolkit;
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#addPart(org.eclipse.ui.forms.IFormPart)
+        */
+       public void addPart(IFormPart part) {
+               parts.add(part);
+               part.initialize(this);
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#removePart(org.eclipse.ui.forms.IFormPart)
+        */
+       public void removePart(IFormPart part) {
+               parts.remove(part);
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#getParts()
+        */
+       public IFormPart[] getParts() {
+               return (IFormPart[]) parts.toArray(new IFormPart[parts.size()]);
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#getToolkit()
+        */
+       public FormToolkit getToolkit() {
+               return toolkit;
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#getForm()
+        */
+       public ScrolledComposite getForm() {
+               return form;
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#reflow(boolean)
+        */
+       public void reflow(boolean changed) {
+//             form.reflow(changed);
+       }
+
+       /**
+        * A part can use this method to notify other parts that implement
+        * IPartSelectionListener about selection changes.
+        * 
+        * @param part
+        *            the part that broadcasts the selection
+        * @param selection
+        *            the selection in the part
+        * @see IPartSelectionListener
+        */
+       public void fireSelectionChanged(IFormPart part, ISelection selection) {
+               for (int i = 0; i < parts.size(); i++) {
+                       IFormPart cpart = (IFormPart) parts.get(i);
+                       if (part.equals(cpart))
+                               continue;
+//                     if (cpart instanceof IPartSelectionListener) {
+//                             ((IPartSelectionListener) cpart).selectionChanged(part,
+//                                             selection);
+//                     }
+               }
+       }
+
+       /**
+        * Initializes the form by looping through the managed parts and
+        * initializing them. Has no effect if already called once.
+        */
+       public void initialize() {
+               if (initialized)
+                       return;
+               for (int i = 0; i < parts.size(); i++) {
+                       IFormPart part = (IFormPart) parts.get(i);
+                       part.initialize(this);
+               }
+               initialized = true;
+       }
+
+       /**
+        * Disposes all the parts in this form.
+        */
+       public void dispose() {
+               for (int i = 0; i < parts.size(); i++) {
+                       IFormPart part = (IFormPart) parts.get(i);
+                       part.dispose();
+               }
+               if (ownsToolkit) {
+                       toolkit.dispose();
+               }
+       }
+
+       /**
+        * Refreshes the form by refreshes all the stale parts. Since 3.1, this
+        * method is performed on a UI thread when called from another thread so it
+        * is not needed to wrap the call in <code>Display.syncExec</code> or
+        * <code>asyncExec</code>.
+        */
+       public void refresh() {
+               Thread t = Thread.currentThread();
+               Thread dt = toolkit.getColors().getDisplay().getThread();
+               if (t.equals(dt))
+                       doRefresh();
+               else {
+                       toolkit.getColors().getDisplay().asyncExec(new Runnable() {
+                               public void run() {
+                                       doRefresh();
+                               }
+                       });
+               }
+       }
+
+       private void doRefresh() {
+               int nrefreshed = 0;
+               for (int i = 0; i < parts.size(); i++) {
+                       IFormPart part = (IFormPart) parts.get(i);
+                       if (part.isStale()) {
+                               part.refresh();
+                               nrefreshed++;
+                       }
+               }
+//             if (nrefreshed > 0)
+//                     form.reflow(true);
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#commit(boolean)
+        */
+       public void commit(boolean onSave) {
+               for (int i = 0; i < parts.size(); i++) {
+                       IFormPart part = (IFormPart) parts.get(i);
+                       if (part.isDirty())
+                               part.commit(onSave);
+               }
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#setInput(java.lang.Object)
+        */
+       public boolean setInput(Object input) {
+               boolean pageResult = false;
+
+               this.input = input;
+               for (int i = 0; i < parts.size(); i++) {
+                       IFormPart part = (IFormPart) parts.get(i);
+                       boolean result = part.setFormInput(input);
+                       if (result)
+                               pageResult = true;
+               }
+               return pageResult;
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#getInput()
+        */
+       public Object getInput() {
+               return input;
+       }
+
+       /**
+        * Transfers the focus to the first form part.
+        */
+       public void setFocus() {
+               if (parts.size() > 0) {
+                       IFormPart part = (IFormPart) parts.get(0);
+                       part.setFocus();
+               }
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#isDirty()
+        */
+       public boolean isDirty() {
+               for (int i = 0; i < parts.size(); i++) {
+                       IFormPart part = (IFormPart) parts.get(i);
+                       if (part.isDirty())
+                               return true;
+               }
+               return false;
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#isStale()
+        */
+       public boolean isStale() {
+               for (int i = 0; i < parts.size(); i++) {
+                       IFormPart part = (IFormPart) parts.get(i);
+                       if (part.isStale())
+                               return true;
+               }
+               return false;
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#dirtyStateChanged()
+        */
+       public void dirtyStateChanged() {
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#staleStateChanged()
+        */
+       public void staleStateChanged() {
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#getContainer()
+        */
+       public Object getContainer() {
+               return container;
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#setContainer(java.lang.Object)
+        */
+       public void setContainer(Object container) {
+               this.container = container;
+       }
+
+       /* (non-Javadoc)
+        * @see org.eclipse.ui.forms.IManagedForm#getMessageManager()
+        */
+//     public IMessageManager getMessageManager() {
+//             return form.getMessageManager();
+//     }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/FormEditor.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/FormEditor.java
new file mode 100644 (file)
index 0000000..484dae8
--- /dev/null
@@ -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.
+ * <p>
+ * Pages are added 'lazily' i.e. adding a page reserves a tab for it but does
+ * not cause the page control to be created. Page control is created when an
+ * attempt is made to select the page in question. This allows editors with
+ * several tabs and complex pages to open quickly.
+ * <p>
+ * Subclasses should extend this class and implement <code>addPages</code>
+ * method. One of the two <code>addPage</code> methods should be called to
+ * contribute pages to the editor. One adds complete (standalone) editors as
+ * nested tabs. These editors will be created right away and will be hooked so
+ * that key bindings, selection service etc. is compatible with the one for the
+ * standalone case. The other method adds classes that implement
+ * <code>IFormPage</code> interface. These pages will be created lazily and
+ * they will share the common key binding and selection service. Since 3.1,
+ * FormEditor is a page change provider. It allows listeners to attach to it and
+ * get notified when pages are changed. This new API in JFace allows dynamic
+ * help to update on page changes.
+ * 
+ * @since 1.0
+ */
+// RAP [if] As RAP is still using workbench 3.4, the implementation of
+// IPageChangeProvider is missing from MultiPageEditorPart. Remove this code
+// with the adoption of workbench > 3.5
+//public abstract class FormEditor extends MultiPageEditorPart  {
+public abstract class FormEditor  implements
+        IPageChangeProvider {
+       private FormToolkit formToolkit;
+       
+       
+public FormToolkit getToolkit() {
+               return formToolkit;
+       }
+
+public void editorDirtyStateChanged() {
+       
+}
+
+public FormPage getActivePageInstance() {
+       return null;
+}
+
+       // RAP [if] As RAP is still using workbench 3.4, the implementation of
+// IPageChangeProvider is missing from MultiPageEditorPart. Remove this code
+// with the adoption of workbench > 3.5
+//     private ListenerList pageListeners = new ListenerList();
+//     
+//    /*
+//     * (non-Javadoc)
+//     * 
+//     * @see org.eclipse.jface.dialogs.IPageChangeProvider#addPageChangedListener(org.eclipse.jface.dialogs.IPageChangedListener)
+//     */
+//    public void addPageChangedListener(IPageChangedListener listener) {
+//        pageListeners.add(listener);
+//    }
+//
+//    /*
+//     * (non-Javadoc)
+//     * 
+//     * @see org.eclipse.jface.dialogs.IPageChangeProvider#removePageChangedListener(org.eclipse.jface.dialogs.IPageChangedListener)
+//     */
+//    public void removePageChangedListener(IPageChangedListener listener) {
+//        pageListeners.remove(listener);
+//    }
+//    
+//     private void firePageChanged(final PageChangedEvent event) {
+//        Object[] listeners = pageListeners.getListeners();
+//        for (int i = 0; i < listeners.length; ++i) {
+//            final IPageChangedListener l = (IPageChangedListener) listeners[i];
+//            SafeRunnable.run(new SafeRunnable() {
+//                public void run() {
+//                    l.pageChanged(event);
+//                }
+//            });
+//        }
+//    }
+// RAPEND [if]
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/FormPage.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/FormPage.java
new file mode 100644 (file)
index 0000000..a788412
--- /dev/null
@@ -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
+        * <code>initialize</code> method if this constructor is used.
+        * 
+        * @param id
+        *            a unique page identifier
+        * @param title
+        *            a user-friendly page title
+        */
+       public FormPage(String id, String title) {
+               this.id = id;
+               setPartName(title);
+       }
+       /**
+        * Initializes the form page.
+        * 
+        * @see IEditorPart#init
+        */
+//     public void init(IEditorSite site, IEditorInput input) {
+//             setSite(site);
+//             setInput(input);
+//     }
+       /**
+        * Primes the form page with the parent editor instance.
+        * 
+        * @param editor
+        *            the parent editor
+        */
+       public void initialize(FormEditor editor) {
+               this.editor = editor;
+       }
+       /**
+        * Returns the parent editor.
+        * 
+        * @return parent editor instance
+        */
+       public FormEditor getEditor() {
+               return editor;
+       }
+       /**
+        * Returns the managed form owned by this page.
+        * 
+        * @return the managed form
+        */
+       public IManagedForm getManagedForm() {
+               return mform;
+       }
+       /**
+        * Implements the required method by refreshing the form when set active.
+        * Subclasses must call super when overriding this method.
+        */
+       public void setActive(boolean active) {
+               if (active) {
+                       // We are switching to this page - refresh it
+                       // if needed.
+                       if (mform != null)
+                               mform.refresh();
+               }
+       }
+       /**
+        * Tests if the page is active by asking the parent editor if this page is
+        * the currently active page.
+        * 
+        * @return <code>true</code> if the page is currently active,
+        *         <code>false</code> otherwise.
+        */
+       public boolean isActive() {
+               return this.equals(editor.getActivePageInstance());
+       }
+       /**
+        * Creates the part control by creating the managed form using the parent
+        * editor's toolkit. Subclasses should override
+        * <code>createFormContent(IManagedForm)</code> to populate the form with
+        * content.
+        * 
+        * @param parent
+        *            the page parent composite
+        */
+       public void createPartControl(Composite parent) {
+               ScrolledComposite form = editor.getToolkit().createScrolledForm(parent);
+               mform = new PageForm(this, form);
+               BusyIndicator.showWhile(parent.getDisplay(), new Runnable() {
+                       public void run() {
+                               createFormContent(mform);
+                       }
+               });
+       }
+       /**
+        * Subclasses should override this method to create content in the form
+        * hosted in this page.
+        * 
+        * @param managedForm
+        *            the form hosted in this page.
+        */
+       protected void createFormContent(IManagedForm managedForm) {
+       }
+       /**
+        * Returns the form page control.
+        * 
+        * @return managed form's control
+        */
+       public Control getPartControl() {
+               return mform != null ? mform.getForm() : null;
+       }
+       /**
+        * Disposes the managed form.
+        */
+       public void dispose() {
+               if (mform != null)
+                       mform.dispose();
+       }
+       /**
+        * Returns the unique identifier that can be used to reference this page.
+        * 
+        * @return the unique page identifier
+        */
+       public String getId() {
+               return id;
+       }
+       /**
+        * Returns <code>null</code>- form page has no title image. Subclasses
+        * may override.
+        * 
+        * @return <code>null</code>
+        */
+       public Image getTitleImage() {
+               return null;
+       }
+       /**
+        * Sets the focus by delegating to the managed form.
+        */
+       public void setFocus() {
+               if (mform != null)
+                       mform.setFocus();
+       }
+       /**
+        * @see org.eclipse.ui.ISaveablePart#doSave(org.eclipse.core.runtime.IProgressMonitor)
+        */
+//     public void doSave(IProgressMonitor monitor) {
+//             if (mform != null)
+//                     mform.commit(true);
+//     }
+       /**
+        * @see org.eclipse.ui.ISaveablePart#doSaveAs()
+        */
+       public void doSaveAs() {
+       }
+       /**
+        * @see org.eclipse.ui.ISaveablePart#isSaveAsAllowed()
+        */
+       public boolean isSaveAsAllowed() {
+               return false;
+       }
+       /**
+        * Implemented by testing if the managed form is dirty.
+        * 
+        * @return <code>true</code> if the managed form is dirty,
+        *         <code>false</code> otherwise.
+        * 
+        * @see org.eclipse.ui.ISaveablePart#isDirty()
+        */
+       public boolean isDirty() {
+               return mform != null ? mform.isDirty() : false;
+       }
+       /**
+        * Preserves the page index.
+        * 
+        * @param index
+        *            the assigned page index
+        */
+       public void setIndex(int index) {
+               this.index = index;
+       }
+       /**
+        * Returns the saved page index.
+        * 
+        * @return the page index
+        */
+       public int getIndex() {
+               return index;
+       }
+       /**
+        * Form pages are not editors.
+        * 
+        * @return <code>false</code>
+        */
+       public boolean isEditor() {
+               return false;
+       }
+       /**
+        * Attempts to select and reveal the given object by passing the request to
+        * the managed form.
+        * 
+        * @param object
+        *            the object to select and reveal in the page if possible.
+        * @return <code>true</code> if the page has been successfully selected
+        *         and revealed by one of the managed form parts, <code>false</code>
+        *         otherwise.
+        */
+       public boolean selectReveal(Object object) {
+               if (mform != null)
+                       return mform.setInput(object);
+               return false;
+       }
+       /**
+        * By default, editor will be allowed to flip the page.
+        * @return <code>true</code>
+        */
+       public boolean canLeaveThePage() {
+               return true;
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/IFormPage.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/IFormPage.java
new file mode 100644 (file)
index 0000000..eb08cb5
--- /dev/null
@@ -0,0 +1,119 @@
+package org.argeo.cms.ui.eclipse.forms.editor;
+import org.argeo.cms.ui.eclipse.forms.IManagedForm;
+import org.eclipse.swt.widgets.Control;
+/**
+ * Interface that all GUI pages need to implement in order
+ * to be added to FormEditor part. The interface makes 
+ * several assumptions:
+ * <ul>
+ * <li>The form page has a managed form</li>
+ * <li>The form page has a unique id</li>
+ * <li>The form page can be GUI but can also wrap a complete
+ * editor class (in that case, it should return <code>true</code>
+ * from <code>isEditor()</code> method).</li>
+ * <li>The form page is lazy i.e. understands that 
+ * its part control will be created at the last possible
+ * moment.</li>.
+ * </ul>
+ * <p>Existing editors can be wrapped by implementing
+ * this interface. In this case, 'isEditor' should return <code>true</code>.
+ * A common editor to wrap in <code>TextEditor</code> that is
+ * often added to show the raw source code of the file open into
+ * the multi-page editor.
+ * 
+ * @since 1.0
+ */
+public interface IFormPage {
+       /**
+        * @param editor
+        *            the form editor that this page belongs to
+        */
+       void initialize(FormEditor editor);
+       /**
+        * Returns the editor this page belongs to.
+        * 
+        * @return the form editor
+        */
+       FormEditor getEditor();
+       /**
+        * Returns the managed form of this page, unless this is a source page.
+        * 
+        * @return the managed form or <samp>null </samp> if this is a source page.
+        */
+       IManagedForm getManagedForm();
+       /**
+        * Indicates whether the page has become the active in the editor. Classes
+        * that implement this interface may use this method to commit the page (on
+        * <code>false</code>) or lazily create and/or populate the content on
+        * <code>true</code>.
+        * 
+        * @param active
+        *            <code>true</code> if page should be visible, <code>false</code>
+        *            otherwise.
+        */
+       void setActive(boolean active);
+       /**
+        * Returns <samp>true </samp> if page is currently active, false if not.
+        * 
+        * @return <samp>true </samp> for active page.
+        */
+       boolean isActive();
+       /**
+        * Tests if the content of the page is in a state that allows the
+        * editor to flip to another page. Typically, pages that contain
+        * raw source with syntax errors should not allow editors to 
+        * leave them until errors are corrected.
+        * @return <code>true</code> if the editor can flip to another page,
+        * <code>false</code> otherwise.
+        */
+       boolean canLeaveThePage();
+       /**
+        * Returns the control associated with this page.
+        * 
+        * @return the control of this page if created or <samp>null </samp> if the
+        *         page has not been shown yet.
+        */
+       Control getPartControl();
+       /**
+        * Page must have a unique id that can be used to show it without knowing
+        * its relative position in the editor.
+        * 
+        * @return the unique page identifier
+        */
+       String getId();
+       /**
+        * Returns the position of the page in the editor.
+        * 
+        * @return the zero-based index of the page in the editor.
+        */
+       int getIndex();
+       /**
+        * Sets the position of the page in the editor.
+        * 
+        * @param index
+        *            the zero-based index of the page in the editor.
+        */
+       void setIndex(int index);
+       /**
+        * Tests whether this page wraps a complete editor that
+        * can be registered on its own, or represents a page
+        * that cannot exist outside the multi-page editor context.
+        * 
+        * @return <samp>true </samp> if the page wraps an editor,
+        *         <samp>false </samp> if this is a form page.
+        */
+       boolean isEditor();
+       /**
+        * A hint to bring the provided object into focus. If the object is in a
+        * tree or table control, select it. If it is shown on a scrollable page,
+        * ensure that it is visible. If the object is not presented in 
+        * the page, <code>false</code> should be returned to allow another
+        * page to try.
+        * 
+        * @param object
+        *            object to select and reveal
+        * @return <code>true</code> if the request was successful, <code>false</code>
+        *         otherwise.
+        */
+       boolean selectReveal(Object object);
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/DefaultRepositoryRegister.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/DefaultRepositoryRegister.java
new file mode 100644 (file)
index 0000000..3806341
--- /dev/null
@@ -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<String, Repository> repositories = Collections.unmodifiableMap(new TreeMap<String, Repository>());
+
+       @SuppressWarnings("rawtypes")
+       public synchronized Repository getRepository(Map parameters) throws RepositoryException {
+               if (!parameters.containsKey(CN))
+                       throw new RepositoryException("Parameter " + CN + " has to be defined.");
+               String alias = parameters.get(CN).toString();
+               if (!repositories.containsKey(alias))
+                       throw new RepositoryException("No repository registered with alias " + alias);
+
+               return repositories.get(alias);
+       }
+
+       /** Access to the read-only map */
+       public synchronized Map<String, Repository> getRepositories() {
+               return repositories;
+       }
+
+       /** Registers a service, typically called when OSGi services are bound. */
+       @SuppressWarnings("rawtypes")
+       public synchronized void register(Repository repository, Map properties) {
+               String alias;
+               if (properties == null || !properties.containsKey(CN)) {
+                       log.warn("Cannot register a repository if no " + CN + " property is specified.");
+                       return;
+               }
+               alias = properties.get(CN).toString();
+               Map<String, Repository> map = new TreeMap<String, Repository>(repositories);
+               map.put(alias, repository);
+               repositories = Collections.unmodifiableMap(map);
+               setChanged();
+               notifyObservers(alias);
+       }
+
+       /** Unregisters a service, typically called when OSGi services are unbound. */
+       @SuppressWarnings("rawtypes")
+       public synchronized void unregister(Repository repository, Map properties) {
+               // TODO: also check bean name?
+               if (properties == null || !properties.containsKey(CN)) {
+                       log.warn("Cannot unregister a repository without property " + CN);
+                       return;
+               }
+
+               String alias = properties.get(CN).toString();
+               Map<String, Repository> map = new TreeMap<String, Repository>(repositories);
+               if (map.remove(alias) == null) {
+                       log.warn("No repository was registered with alias " + alias);
+                       return;
+               }
+               repositories = Collections.unmodifiableMap(map);
+               setChanged();
+               notifyObservers(alias);
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/FullVersioningTreeContentProvider.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/FullVersioningTreeContentProvider.java
new file mode 100644 (file)
index 0000000..0f7ee77
--- /dev/null
@@ -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<Version> result = new ArrayList<Version>();
+                       VersionIterator vi = vh.getAllLinearVersions();
+
+                       while (vi.hasNext()) {
+                               result.add(vi.nextVersion());
+                       }
+                       return result.toArray();
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException(
+                                       "Unexpected error while getting version elements", re);
+               }
+       }
+
+       public Object[] getChildren(Object parentElement) {
+               try {
+                       if (parentElement instanceof Version) {
+                               List<Node> tmp = new ArrayList<Node>();
+                               tmp.add(((Version) parentElement).getFrozenNode());
+                               return tmp.toArray();
+                       }
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Unexpected error while getting child "
+                                       + "node for version element", re);
+               }
+               return null;
+       }
+
+       public Object getParent(Object element) {
+               try {
+                       // this will not work in a simpleVersionning environment, parent is
+                       // not a node.
+                       if (element instanceof Node
+                                       && ((Node) element).isNodeType(NodeType.NT_FROZEN_NODE)) {
+                               Node node = (Node) element;
+                               return node.getParent();
+                       } else
+                               return null;
+               } catch (RepositoryException e) {
+                       return null;
+               }
+       }
+
+       public boolean hasChildren(Object element) {
+               try {
+                       if (element instanceof Version)
+                               return true;
+                       else if (element instanceof Node)
+                               return ((Node) element).hasNodes();
+                       else
+                               return false;
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot check children of " + element, e);
+               }
+       }
+
+       public void dispose() {
+       }
+
+       public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+       }
+
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrBrowserUtils.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrBrowserUtils.java
new file mode 100644 (file)
index 0000000..b36acc3
--- /dev/null
@@ -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/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrDClickListener.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrDClickListener.java
new file mode 100644 (file)
index 0000000..1707681
--- /dev/null
@@ -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/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrImages.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrImages.java
new file mode 100644 (file)
index 0000000..d1d1e31
--- /dev/null
@@ -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/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrTreeContentProvider.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrTreeContentProvider.java
new file mode 100644 (file)
index 0000000..cc8479f
--- /dev/null
@@ -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<Node> children = new ArrayList<Node>();
+                       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/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/NodeContentProvider.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/NodeContentProvider.java
new file mode 100644 (file)
index 0000000..0c1221a
--- /dev/null
@@ -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.api.cms.keyring.Keyring;
+import org.argeo.cms.jcr.CmsJcrUtils;
+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<Object> objs = new ArrayList<Object>();
+               if (homeNode != null)
+                       objs.add(homeNode);
+               if (repositoriesNode != null)
+                       objs.add(repositoriesNode);
+               return objs.toArray();
+       }
+
+       public Object[] getChildren(Object parentElement) {
+               if (parentElement instanceof TreeParent) {
+                       if (sortChildren) {
+                               Object[] tmpArr = ((TreeParent) parentElement).getChildren();
+                               if (tmpArr == null)
+                                       return new Object[0];
+                               TreeParent[] arr = new TreeParent[tmpArr.length];
+                               for (int i = 0; i < tmpArr.length; i++)
+                                       arr[i] = (TreeParent) tmpArr[i];
+                               Arrays.sort(arr, itemComparator);
+                               return arr;
+                       } else
+                               return ((TreeParent) parentElement).getChildren();
+               } else
+                       return new Object[0];
+       }
+
+       /**
+        * Sets whether the content provider should order the children nodes or not.
+        * It is user duty to call a full refresh of the tree after changing this
+        * parameter.
+        */
+       public void setSortChildren(boolean sortChildren) {
+               this.sortChildren = sortChildren;
+       }
+
+       public Object getParent(Object element) {
+               if (element instanceof TreeParent) {
+                       return ((TreeParent) element).getParent();
+               } else
+                       return null;
+       }
+
+       public boolean hasChildren(Object element) {
+               if (element instanceof RepositoriesElem) {
+                       RepositoryRegister rr = ((RepositoriesElem) element)
+                                       .getRepositoryRegister();
+                       return rr.getRepositories().size() > 0;
+               } else if (element instanceof TreeParent) {
+                       TreeParent tp = (TreeParent) element;
+                       return tp.hasChildren();
+               }
+               return false;
+       }
+
+       public void dispose() {
+               if (homeNode != null)
+                       homeNode.dispose();
+               if (repositoriesNode != null) {
+                       // logs out open sessions
+                       // see https://bugzilla.argeo.org/show_bug.cgi?id=23
+                       repositoriesNode.dispose();
+               }
+       }
+
+       /**
+        * Specific comparator for this view. See specification here:
+        * https://www.argeo.org/bugzilla/show_bug.cgi?id=139
+        */
+       private class TreeBrowserComparator implements Comparator<TreeParent> {
+
+               public int category(TreeParent element) {
+                       if (element instanceof SingleJcrNodeElem) {
+                               Node node = ((SingleJcrNodeElem) element).getNode();
+                               try {
+                                       if (node.isNodeType(NodeType.NT_FOLDER))
+                                               return 5;
+                               } catch (RepositoryException e) {
+                                       // TODO Auto-generated catch block
+                                       e.printStackTrace();
+                               }
+                       }
+                       return 10;
+               }
+
+               public int compare(TreeParent o1, TreeParent o2) {
+                       int cat1 = category(o1);
+                       int cat2 = category(o2);
+
+                       if (cat1 != cat2) {
+                               return cat1 - cat2;
+                       }
+                       return o1.getName().compareTo(o2.getName());
+               }
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/NodeLabelProvider.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/NodeLabelProvider.java
new file mode 100644 (file)
index 0000000..a5751c0
--- /dev/null
@@ -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/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/OsgiRepositoryRegister.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/OsgiRepositoryRegister.java
new file mode 100644 (file)
index 0000000..444350a
--- /dev/null
@@ -0,0 +1,52 @@
+package org.argeo.cms.ui.jcr;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Repository;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class OsgiRepositoryRegister extends DefaultRepositoryRegister {
+       private final static BundleContext bc = FrameworkUtil.getBundle(OsgiRepositoryRegister.class).getBundleContext();
+       private final ServiceTracker<Repository, Repository> repositoryTracker;
+
+       public OsgiRepositoryRegister() {
+               repositoryTracker = new ServiceTracker<Repository, Repository>(bc, Repository.class, null) {
+
+                       @Override
+                       public Repository addingService(ServiceReference<Repository> reference) {
+
+                               Repository repository = super.addingService(reference);
+                               Map<String, Object> props = new HashMap<>();
+                               for (String key : reference.getPropertyKeys()) {
+                                       props.put(key, reference.getProperty(key));
+                               }
+                               register(repository, props);
+                               return repository;
+                       }
+
+                       @Override
+                       public void removedService(ServiceReference<Repository> reference, Repository service) {
+                               Map<String, Object> props = new HashMap<>();
+                               for (String key : reference.getPropertyKeys()) {
+                                       props.put(key, reference.getProperty(key));
+                               }
+                               unregister(service, props);
+                               super.removedService(reference, service);
+                       }
+
+               };
+       }
+
+       public void init() {
+               repositoryTracker.open();
+       }
+
+       public void destroy() {
+               repositoryTracker.close();
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/PropertiesContentProvider.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/PropertiesContentProvider.java
new file mode 100644 (file)
index 0000000..fd544bb
--- /dev/null
@@ -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<Property> props = new TreeSet<Property>(itemComparator);
+                               PropertyIterator pit = ((Node) inputElement).getProperties();
+                               while (pit.hasNext())
+                                       props.add(pit.nextProperty());
+                               return props.toArray();
+                       }
+                       return new Object[] {};
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot get element for "
+                                       + inputElement, e);
+               }
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/PropertyLabelProvider.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/PropertyLabelProvider.java
new file mode 100644 (file)
index 0000000..58d6031
--- /dev/null
@@ -0,0 +1,102 @@
+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.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;
+
+       private final static String DATE_TIME_FORMAT = "dd/MM/yyyy, HH:mm";
+
+       // Utils
+       protected DateFormat timeFormatter = new SimpleDateFormat(DATE_TIME_FORMAT);
+
+       public void update(ViewerCell cell) {
+               Object element = cell.getElement();
+               cell.setText(getColumnText(element, cell.getColumnIndex()));
+       }
+
+       public String getColumnText(Object element, int columnIndex) {
+               try {
+                       if (element instanceof Property) {
+                               Property prop = (Property) element;
+                               if (prop.isMultiple()) {
+                                       switch (columnIndex) {
+                                       case COLUMN_PROPERTY:
+                                               return prop.getName();
+                                       case COLUMN_VALUE:
+                                               // Corresponding values are listed on children
+                                               return "";
+                                       case COLUMN_TYPE:
+                                               return JcrBrowserUtils.getPropertyTypeAsString(prop);
+                                       case COLUMN_ATTRIBUTES:
+                                               return JcrUtils.getPropertyDefinitionAsString(prop);
+                                       }
+                               } else {
+                                       switch (columnIndex) {
+                                       case COLUMN_PROPERTY:
+                                               return prop.getName();
+                                       case COLUMN_VALUE:
+                                               return formatValueAsString(prop.getValue());
+                                       case COLUMN_TYPE:
+                                               return JcrBrowserUtils.getPropertyTypeAsString(prop);
+                                       case COLUMN_ATTRIBUTES:
+                                               return JcrUtils.getPropertyDefinitionAsString(prop);
+                                       }
+                               }
+                       } else if (element instanceof Value) {
+                               Value val = (Value) element;
+                               switch (columnIndex) {
+                               case COLUMN_PROPERTY:
+                                       // Nothing to show
+                                       return "";
+                               case COLUMN_VALUE:
+                                       return formatValueAsString(val);
+                               case COLUMN_TYPE:
+                                       // listed on the parent
+                                       return "";
+                               case COLUMN_ATTRIBUTES:
+                                       // Corresponding attributes are listed on the parent
+                                       return "";
+                               }
+                       }
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Cannot retrieve prop value on " + element, re);
+               }
+               return null;
+       }
+
+       private String formatValueAsString(Value value) {
+               // TODO enhance this method
+               try {
+                       String strValue;
+
+                       if (value.getType() == PropertyType.BINARY)
+                               strValue = "<binary>";
+                       else if (value.getType() == PropertyType.DATE)
+                               strValue = timeFormatter.format(value.getDate().getTime());
+                       else
+                               strValue = value.getString();
+                       return strValue;
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("unexpected error while formatting value", e);
+               }
+       }
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/RepositoryRegister.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/RepositoryRegister.java
new file mode 100644 (file)
index 0000000..802c756
--- /dev/null
@@ -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<String, Repository> getRepositories();
+}
diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/VersionLabelProvider.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/VersionLabelProvider.java
new file mode 100644 (file)
index 0000000..37dfe2b
--- /dev/null
@@ -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/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/MaintainedRepositoryElem.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/MaintainedRepositoryElem.java
new file mode 100644 (file)
index 0000000..d33b33f
--- /dev/null
@@ -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/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RemoteRepositoryElem.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RemoteRepositoryElem.java
new file mode 100644 (file)
index 0000000..382f356
--- /dev/null
@@ -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.api.cms.keyring.Keyring;
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.jcr.CmsJcrUtils;
+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/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RepositoriesElem.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RepositoriesElem.java
new file mode 100644 (file)
index 0000000..6218e04
--- /dev/null
@@ -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.api.cms.keyring.Keyring;
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.jcr.CmsJcrUtils;
+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<String, Repository> refRepos = repositoryRegister.getRepositories();
+                       for (String name : refRepos.keySet()) {
+                               Repository repository = refRepos.get(name);
+                               // if (repository instanceof MaintainedRepository)
+                               // super.addChild(new MaintainedRepositoryElem(name,
+                               // repository, this));
+                               // else
+                               super.addChild(new RepositoryElem(name, repository, this));
+                       }
+
+                       // remote
+                       if (keyring != null) {
+                               try {
+                                       addRemoteRepositories(keyring);
+                               } catch (RepositoryException e) {
+                                       throw new EclipseUiException("Cannot browse remote repositories", e);
+                               }
+                       }
+                       return super.getChildren();
+               }
+       }
+
+       protected void addRemoteRepositories(Keyring jcrKeyring) throws RepositoryException {
+               Node userHome = 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/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RepositoryElem.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RepositoryElem.java
new file mode 100644 (file)
index 0000000..296c369
--- /dev/null
@@ -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/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/SingleJcrNodeElem.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/SingleJcrNodeElem.java
new file mode 100644 (file)
index 0000000..a2584a5
--- /dev/null
@@ -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/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/WorkspaceElem.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/WorkspaceElem.java
new file mode 100644 (file)
index 0000000..2d78666
--- /dev/null
@@ -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/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/package-info.java
new file mode 100644 (file)
index 0000000..8f54744
--- /dev/null
@@ -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/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/package-info.java
new file mode 100644 (file)
index 0000000..26ae330
--- /dev/null
@@ -0,0 +1,2 @@
+/** SWT/JFace JCR components. */
+package org.argeo.cms.ui.jcr;
\ No newline at end of file
diff --git a/swt/org.argeo.tool.swt/.classpath b/swt/org.argeo.tool.swt/.classpath
new file mode 100644 (file)
index 0000000..81fe078
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/swt/org.argeo.tool.swt/.project b/swt/org.argeo.tool.swt/.project
new file mode 100644 (file)
index 0000000..0ffd546
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.tool.swt</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/swt/org.argeo.tool.swt/bnd.bnd b/swt/org.argeo.tool.swt/bnd.bnd
new file mode 100644 (file)
index 0000000..2b2a02f
--- /dev/null
@@ -0,0 +1 @@
+Bundle-ActivationPolicy: lazy
diff --git a/swt/org.argeo.tool.swt/build.properties b/swt/org.argeo.tool.swt/build.properties
new file mode 100644 (file)
index 0000000..34d2e4d
--- /dev/null
@@ -0,0 +1,4 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .
diff --git a/swt/org.argeo.tool.swt/icons/actions/add.png b/swt/org.argeo.tool.swt/icons/actions/add.png
new file mode 100644 (file)
index 0000000..5c06bf0
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/actions/add.png differ
diff --git a/swt/org.argeo.tool.swt/icons/actions/close-all.png b/swt/org.argeo.tool.swt/icons/actions/close-all.png
new file mode 100644 (file)
index 0000000..81bfc95
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/actions/close-all.png differ
diff --git a/swt/org.argeo.tool.swt/icons/actions/delete.png b/swt/org.argeo.tool.swt/icons/actions/delete.png
new file mode 100644 (file)
index 0000000..9712723
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/actions/delete.png differ
diff --git a/swt/org.argeo.tool.swt/icons/actions/edit.png b/swt/org.argeo.tool.swt/icons/actions/edit.png
new file mode 100644 (file)
index 0000000..ad3db9f
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/actions/edit.png differ
diff --git a/swt/org.argeo.tool.swt/icons/actions/save-all.png b/swt/org.argeo.tool.swt/icons/actions/save-all.png
new file mode 100644 (file)
index 0000000..f48ed32
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/actions/save-all.png differ
diff --git a/swt/org.argeo.tool.swt/icons/actions/save.png b/swt/org.argeo.tool.swt/icons/actions/save.png
new file mode 100644 (file)
index 0000000..1c58ada
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/actions/save.png differ
diff --git a/swt/org.argeo.tool.swt/icons/active.gif b/swt/org.argeo.tool.swt/icons/active.gif
new file mode 100644 (file)
index 0000000..7d24707
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/active.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/add.gif b/swt/org.argeo.tool.swt/icons/add.gif
new file mode 100644 (file)
index 0000000..252d7eb
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/add.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/add.png b/swt/org.argeo.tool.swt/icons/add.png
new file mode 100644 (file)
index 0000000..c7edfec
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/add.png differ
diff --git a/swt/org.argeo.tool.swt/icons/addFolder.gif b/swt/org.argeo.tool.swt/icons/addFolder.gif
new file mode 100644 (file)
index 0000000..d3f43d9
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/addFolder.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/addPrivileges.gif b/swt/org.argeo.tool.swt/icons/addPrivileges.gif
new file mode 100644 (file)
index 0000000..a6b251f
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/addPrivileges.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/addRepo.gif b/swt/org.argeo.tool.swt/icons/addRepo.gif
new file mode 100644 (file)
index 0000000..26d81c0
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/addRepo.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/addWorkspace.png b/swt/org.argeo.tool.swt/icons/addWorkspace.png
new file mode 100644 (file)
index 0000000..bbee775
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/addWorkspace.png differ
diff --git a/swt/org.argeo.tool.swt/icons/adminLog.gif b/swt/org.argeo.tool.swt/icons/adminLog.gif
new file mode 100644 (file)
index 0000000..6ef3bca
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/adminLog.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/batch.gif b/swt/org.argeo.tool.swt/icons/batch.gif
new file mode 100644 (file)
index 0000000..b8ca14a
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/batch.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/begin.gif b/swt/org.argeo.tool.swt/icons/begin.gif
new file mode 100755 (executable)
index 0000000..feb8e94
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/begin.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/binary.png b/swt/org.argeo.tool.swt/icons/binary.png
new file mode 100644 (file)
index 0000000..fdf4f82
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/binary.png differ
diff --git a/swt/org.argeo.tool.swt/icons/browser.gif b/swt/org.argeo.tool.swt/icons/browser.gif
new file mode 100644 (file)
index 0000000..6c7320c
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/browser.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/bundles.gif b/swt/org.argeo.tool.swt/icons/bundles.gif
new file mode 100644 (file)
index 0000000..e9a6bd9
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/bundles.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/changePassword.gif b/swt/org.argeo.tool.swt/icons/changePassword.gif
new file mode 100644 (file)
index 0000000..274a850
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/changePassword.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/clear.gif b/swt/org.argeo.tool.swt/icons/clear.gif
new file mode 100644 (file)
index 0000000..6bc10f9
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/clear.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/close-all.png b/swt/org.argeo.tool.swt/icons/close-all.png
new file mode 100644 (file)
index 0000000..85d4d42
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/close-all.png differ
diff --git a/swt/org.argeo.tool.swt/icons/commit.gif b/swt/org.argeo.tool.swt/icons/commit.gif
new file mode 100755 (executable)
index 0000000..876f3eb
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/commit.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/delete.png b/swt/org.argeo.tool.swt/icons/delete.png
new file mode 100644 (file)
index 0000000..676a39d
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/delete.png differ
diff --git a/swt/org.argeo.tool.swt/icons/dumpNode.gif b/swt/org.argeo.tool.swt/icons/dumpNode.gif
new file mode 100644 (file)
index 0000000..14eb1be
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/dumpNode.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/file.gif b/swt/org.argeo.tool.swt/icons/file.gif
new file mode 100644 (file)
index 0000000..ef30288
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/file.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/folder.gif b/swt/org.argeo.tool.swt/icons/folder.gif
new file mode 100644 (file)
index 0000000..42e027c
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/folder.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/getSize.gif b/swt/org.argeo.tool.swt/icons/getSize.gif
new file mode 100644 (file)
index 0000000..b05bf3e
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/getSize.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/group.png b/swt/org.argeo.tool.swt/icons/group.png
new file mode 100644 (file)
index 0000000..cc6683a
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/group.png differ
diff --git a/swt/org.argeo.tool.swt/icons/home.gif b/swt/org.argeo.tool.swt/icons/home.gif
new file mode 100644 (file)
index 0000000..fd0c669
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/home.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/home.png b/swt/org.argeo.tool.swt/icons/home.png
new file mode 100644 (file)
index 0000000..5eb0967
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/home.png differ
diff --git a/swt/org.argeo.tool.swt/icons/import_fs.png b/swt/org.argeo.tool.swt/icons/import_fs.png
new file mode 100644 (file)
index 0000000..d7c890c
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/import_fs.png differ
diff --git a/swt/org.argeo.tool.swt/icons/installed.gif b/swt/org.argeo.tool.swt/icons/installed.gif
new file mode 100644 (file)
index 0000000..2988716
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/installed.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/log.gif b/swt/org.argeo.tool.swt/icons/log.gif
new file mode 100644 (file)
index 0000000..e3ecc55
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/log.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/logout.png b/swt/org.argeo.tool.swt/icons/logout.png
new file mode 100644 (file)
index 0000000..f2952fa
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/logout.png differ
diff --git a/swt/org.argeo.tool.swt/icons/maintenance.gif b/swt/org.argeo.tool.swt/icons/maintenance.gif
new file mode 100644 (file)
index 0000000..e5690ec
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/maintenance.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/node.gif b/swt/org.argeo.tool.swt/icons/node.gif
new file mode 100644 (file)
index 0000000..364c0e7
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/node.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/nodes.gif b/swt/org.argeo.tool.swt/icons/nodes.gif
new file mode 100644 (file)
index 0000000..bba3dbc
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/nodes.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/osgi_explorer.gif b/swt/org.argeo.tool.swt/icons/osgi_explorer.gif
new file mode 100644 (file)
index 0000000..e9a6bd9
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/osgi_explorer.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/password.gif b/swt/org.argeo.tool.swt/icons/password.gif
new file mode 100644 (file)
index 0000000..a6b251f
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/password.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/person-logged-in.png b/swt/org.argeo.tool.swt/icons/person-logged-in.png
new file mode 100644 (file)
index 0000000..87acc14
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/person-logged-in.png differ
diff --git a/swt/org.argeo.tool.swt/icons/person.png b/swt/org.argeo.tool.swt/icons/person.png
new file mode 100644 (file)
index 0000000..7d979a5
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/person.png differ
diff --git a/swt/org.argeo.tool.swt/icons/query.png b/swt/org.argeo.tool.swt/icons/query.png
new file mode 100644 (file)
index 0000000..54c089d
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/query.png differ
diff --git a/swt/org.argeo.tool.swt/icons/refresh.png b/swt/org.argeo.tool.swt/icons/refresh.png
new file mode 100644 (file)
index 0000000..71b3481
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/refresh.png differ
diff --git a/swt/org.argeo.tool.swt/icons/remote_connected.gif b/swt/org.argeo.tool.swt/icons/remote_connected.gif
new file mode 100644 (file)
index 0000000..1492b4e
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/remote_connected.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/remote_disconnected.gif b/swt/org.argeo.tool.swt/icons/remote_disconnected.gif
new file mode 100644 (file)
index 0000000..6c54da9
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/remote_disconnected.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/remove.gif b/swt/org.argeo.tool.swt/icons/remove.gif
new file mode 100644 (file)
index 0000000..0ae6dec
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/remove.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/removePrivileges.gif b/swt/org.argeo.tool.swt/icons/removePrivileges.gif
new file mode 100644 (file)
index 0000000..aa78fd2
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/removePrivileges.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/rename.gif b/swt/org.argeo.tool.swt/icons/rename.gif
new file mode 100644 (file)
index 0000000..8048405
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/rename.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/repositories.gif b/swt/org.argeo.tool.swt/icons/repositories.gif
new file mode 100644 (file)
index 0000000..c13bea1
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/repositories.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/repository_connected.gif b/swt/org.argeo.tool.swt/icons/repository_connected.gif
new file mode 100644 (file)
index 0000000..a15fa55
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/repository_connected.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/repository_disconnected.gif b/swt/org.argeo.tool.swt/icons/repository_disconnected.gif
new file mode 100644 (file)
index 0000000..4576dc5
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/repository_disconnected.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/resolved.gif b/swt/org.argeo.tool.swt/icons/resolved.gif
new file mode 100644 (file)
index 0000000..f4a1ea1
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/resolved.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/role.gif b/swt/org.argeo.tool.swt/icons/role.gif
new file mode 100644 (file)
index 0000000..274a850
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/role.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/rollback.gif b/swt/org.argeo.tool.swt/icons/rollback.gif
new file mode 100755 (executable)
index 0000000..c753995
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/rollback.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/save-all.png b/swt/org.argeo.tool.swt/icons/save-all.png
new file mode 100644 (file)
index 0000000..b68a29b
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/save-all.png differ
diff --git a/swt/org.argeo.tool.swt/icons/save.gif b/swt/org.argeo.tool.swt/icons/save.gif
new file mode 100644 (file)
index 0000000..654ad7b
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/save.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/save.png b/swt/org.argeo.tool.swt/icons/save.png
new file mode 100644 (file)
index 0000000..f27ef2d
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/save.png differ
diff --git a/swt/org.argeo.tool.swt/icons/save_security.png b/swt/org.argeo.tool.swt/icons/save_security.png
new file mode 100644 (file)
index 0000000..ca41dc9
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/save_security.png differ
diff --git a/swt/org.argeo.tool.swt/icons/save_security_disabled.png b/swt/org.argeo.tool.swt/icons/save_security_disabled.png
new file mode 100644 (file)
index 0000000..fb7d08d
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/save_security_disabled.png differ
diff --git a/swt/org.argeo.tool.swt/icons/security.gif b/swt/org.argeo.tool.swt/icons/security.gif
new file mode 100644 (file)
index 0000000..57fb95e
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/security.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/service_published.gif b/swt/org.argeo.tool.swt/icons/service_published.gif
new file mode 100644 (file)
index 0000000..17f771a
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/service_published.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/service_referenced.gif b/swt/org.argeo.tool.swt/icons/service_referenced.gif
new file mode 100644 (file)
index 0000000..c24a95f
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/service_referenced.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/sort.gif b/swt/org.argeo.tool.swt/icons/sort.gif
new file mode 100644 (file)
index 0000000..23c5d0b
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/sort.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/starting.gif b/swt/org.argeo.tool.swt/icons/starting.gif
new file mode 100644 (file)
index 0000000..563743d
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/starting.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/sync.gif b/swt/org.argeo.tool.swt/icons/sync.gif
new file mode 100644 (file)
index 0000000..b4fa052
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/sync.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/user.gif b/swt/org.argeo.tool.swt/icons/user.gif
new file mode 100644 (file)
index 0000000..90a0014
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/user.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/users.gif b/swt/org.argeo.tool.swt/icons/users.gif
new file mode 100644 (file)
index 0000000..2de7edd
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/users.gif differ
diff --git a/swt/org.argeo.tool.swt/icons/workgroup.png b/swt/org.argeo.tool.swt/icons/workgroup.png
new file mode 100644 (file)
index 0000000..7fef996
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/workgroup.png differ
diff --git a/swt/org.argeo.tool.swt/icons/workgroup.xcf b/swt/org.argeo.tool.swt/icons/workgroup.xcf
new file mode 100644 (file)
index 0000000..f517c82
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/workgroup.xcf differ
diff --git a/swt/org.argeo.tool.swt/icons/workspace_connected.png b/swt/org.argeo.tool.swt/icons/workspace_connected.png
new file mode 100644 (file)
index 0000000..0430baa
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/workspace_connected.png differ
diff --git a/swt/org.argeo.tool.swt/icons/workspace_disconnected.png b/swt/org.argeo.tool.swt/icons/workspace_disconnected.png
new file mode 100644 (file)
index 0000000..fddcb8c
Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/workspace_disconnected.png differ
diff --git a/swt/org.argeo.tool.swt/src/org/argeo/cms/ui/theme/CmsImages.java b/swt/org.argeo.tool.swt/src/org/argeo/cms/ui/theme/CmsImages.java
new file mode 100644 (file)
index 0000000..1c4d79e
--- /dev/null
@@ -0,0 +1,49 @@
+package org.argeo.cms.ui.theme;
+
+import java.net.URL;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+
+public class CmsImages {
+       private static BundleContext themeBc = FrameworkUtil.getBundle(CmsImages.class).getBundleContext();
+
+       final public static String ICONS_BASE = "icons/";
+       final public static String TYPES_BASE = ICONS_BASE + "types/";
+       final public static String ACTIONS_BASE = ICONS_BASE + "actions/";
+
+       public static Image createIcon(String name) {
+               return createImg(CmsImages.ICONS_BASE + name);
+       }
+
+       public static Image createAction(String name) {
+               return createImg(CmsImages.ACTIONS_BASE + name);
+       }
+
+       public static Image createType(String name) {
+               return createImg(CmsImages.TYPES_BASE + name);
+       }
+
+       public static Image createImg(String name) {
+               return CmsImages.createDesc(name).createImage(Display.getDefault());
+       }
+
+       public static ImageDescriptor createDesc(String name) {
+               return createDesc(themeBc, name);
+       }
+
+       public static ImageDescriptor createDesc(BundleContext bc, String name) {
+               URL url = bc.getBundle().getResource(name);
+               if (url == null)
+                       return ImageDescriptor.getMissingImageDescriptor();
+               return ImageDescriptor.createFromURL(url);
+       }
+
+       public static Image createImg(BundleContext bc, String name) {
+               return createDesc(bc, name).createImage(Display.getDefault());
+       }
+
+}
diff --git a/swt/org.argeo.tool.swt/src/org/argeo/cms/ui/theme/package-info.java b/swt/org.argeo.tool.swt/src/org/argeo/cms/ui/theme/package-info.java
new file mode 100644 (file)
index 0000000..7d3a260
--- /dev/null
@@ -0,0 +1,2 @@
+/** Argeo CMS core theme images. */
+package org.argeo.cms.ui.theme;
\ No newline at end of file