From: Mathieu Baudier Date: Tue, 11 Jan 2022 05:09:35 +0000 (+0100) Subject: Components-based JCR configuration. X-Git-Tag: argeo-commons-2.3.5~91 X-Git-Url: https://git.argeo.org/?p=lgpl%2Fargeo-commons.git;a=commitdiff_plain;h=0ffddd13968b41b26caddfcf17b3dc86a58eb776 Components-based JCR configuration. --- diff --git a/org.argeo.cms.jcr/OSGI-INF/jcrFsProvider.xml b/org.argeo.cms.jcr/OSGI-INF/jcrFsProvider.xml new file mode 100644 index 000000000..e26453b06 --- /dev/null +++ b/org.argeo.cms.jcr/OSGI-INF/jcrFsProvider.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/org.argeo.cms.jcr/OSGI-INF/jcrRepositoryFactory.xml b/org.argeo.cms.jcr/OSGI-INF/jcrRepositoryFactory.xml new file mode 100644 index 000000000..b43b51920 --- /dev/null +++ b/org.argeo.cms.jcr/OSGI-INF/jcrRepositoryFactory.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/org.argeo.cms.jcr/OSGI-INF/repositoryContextsFactory.xml b/org.argeo.cms.jcr/OSGI-INF/repositoryContextsFactory.xml new file mode 100644 index 000000000..db2bfaa26 --- /dev/null +++ b/org.argeo.cms.jcr/OSGI-INF/repositoryContextsFactory.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/org.argeo.cms.jcr/bnd.bnd b/org.argeo.cms.jcr/bnd.bnd index 761c950e5..71071f697 100644 --- a/org.argeo.cms.jcr/bnd.bnd +++ b/org.argeo.cms.jcr/bnd.bnd @@ -27,6 +27,9 @@ junit.*;resolution:=optional,\ * Service-Component:\ +OSGI-INF/repositoryContextsFactory.xml,\ +OSGI-INF/jcrRepositoryFactory.xml,\ +OSGI-INF/jcrFsProvider.xml,\ OSGI-INF/jcrDeployment.xml,\ OSGI-INF/jcrServletContext.xml,\ OSGI-INF/dataServletContext.xml,\ diff --git a/org.argeo.cms.jcr/build.properties b/org.argeo.cms.jcr/build.properties index 9ef2d5d06..3ddcf97c6 100644 --- a/org.argeo.cms.jcr/build.properties +++ b/org.argeo.cms.jcr/build.properties @@ -1,7 +1,10 @@ output.. = bin/ bin.includes = META-INF/,\ .,\ - OSGI-INF/jcrDeployment.xml + OSGI-INF/jcrDeployment.xml,\ + OSGI-INF/repositoryContextsFactory.xml,\ + OSGI-INF/jcrRepositoryFactory.xml,\ + OSGI-INF/jcrFsProvider.xml source.. = src/ additional.bundles = org.apache.jackrabbit.core,\ javax.jcr,\ diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsFsProvider.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsFsProvider.java deleted file mode 100644 index 50ef25e9a..000000000 --- a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsFsProvider.java +++ /dev/null @@ -1,124 +0,0 @@ -package org.argeo.cms.jcr.internal; - -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.FileSystem; -import java.nio.file.FileSystemAlreadyExistsException; -import java.nio.file.Path; -import java.nio.file.spi.FileSystemProvider; -import java.util.HashMap; -import java.util.Map; - -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.RepositoryFactory; -import javax.jcr.Session; -import javax.jcr.nodetype.NodeType; - -import org.argeo.api.cms.CmsConstants; -import org.argeo.cms.auth.CurrentUser; -import org.argeo.cms.jcr.CmsJcrUtils; -import org.argeo.jackrabbit.fs.AbstractJackrabbitFsProvider; -import org.argeo.jcr.fs.JcrFileSystem; -import org.argeo.jcr.fs.JcrFileSystemProvider; -import org.argeo.jcr.fs.JcrFsException; -import org.osgi.framework.BundleContext; -import org.osgi.framework.FrameworkUtil; -import org.osgi.framework.InvalidSyntaxException; - -/** Implementation of an {@link FileSystemProvider} based on Jackrabbit. */ -public class CmsFsProvider extends AbstractJackrabbitFsProvider { - private Map fileSystems = new HashMap<>(); - - @Override - public String getScheme() { - return CmsConstants.SCHEME_NODE; - } - - @Override - public FileSystem newFileSystem(URI uri, Map env) throws IOException { - BundleContext bc = FrameworkUtil.getBundle(CmsFsProvider.class).getBundleContext(); - String username = CurrentUser.getUsername(); - if (username == null) { - // TODO deal with anonymous - return null; - } - if (fileSystems.containsKey(username)) - throw new FileSystemAlreadyExistsException("CMS file system already exists for user " + username); - - try { - String host = uri.getHost(); - if (host != null && !host.trim().equals("")) { - URI repoUri = new URI("http", uri.getUserInfo(), uri.getHost(), uri.getPort(), "/jcr/node", null, null); - RepositoryFactory repositoryFactory = bc.getService(bc.getServiceReference(RepositoryFactory.class)); - Repository repository = CmsJcrUtils.getRepositoryByUri(repositoryFactory, repoUri.toString()); - CmsFileSystem fileSystem = new CmsFileSystem(this, repository); - fileSystems.put(username, fileSystem); - return fileSystem; - } else { - Repository repository = bc.getService( - bc.getServiceReferences(Repository.class, "(cn=" + CmsConstants.EGO_REPOSITORY + ")") - .iterator().next()); -// Session session = repository.login(); - CmsFileSystem fileSystem = new CmsFileSystem(this, repository); - fileSystems.put(username, fileSystem); - return fileSystem; - } - } catch (InvalidSyntaxException | URISyntaxException e) { - throw new IllegalArgumentException("Cannot open file system " + uri + " for user " + username, e); - } - } - - @Override - public FileSystem getFileSystem(URI uri) { - return currentUserFileSystem(); - } - - @Override - public Path getPath(URI uri) { - JcrFileSystem fileSystem = currentUserFileSystem(); - String path = uri.getPath(); - if (fileSystem == null) - try { - fileSystem = (JcrFileSystem) newFileSystem(uri, new HashMap()); - } catch (IOException e) { - throw new JcrFsException("Could not autocreate file system", e); - } - return fileSystem.getPath(path); - } - - protected JcrFileSystem currentUserFileSystem() { - String username = CurrentUser.getUsername(); - return fileSystems.get(username); - } - - public Node getUserHome(Repository repository) { - try { - Session session = repository.login(CmsConstants.HOME_WORKSPACE); - return CmsJcrUtils.getUserHome(session); - } catch (RepositoryException e) { - throw new IllegalStateException("Cannot get user home", e); - } - } - - static class CmsFileSystem extends JcrFileSystem { - public CmsFileSystem(JcrFileSystemProvider provider, Repository repository) throws IOException { - super(provider, repository); - } - - public boolean skipNode(Node node) throws RepositoryException { -// if (node.isNodeType(NodeType.NT_HIERARCHY_NODE) || node.isNodeType(NodeTypes.NODE_USER_HOME) -// || node.isNodeType(NodeTypes.NODE_GROUP_HOME)) - if (node.isNodeType(NodeType.NT_HIERARCHY_NODE)) - return false; - // FIXME Better identifies home - if (node.hasProperty(Property.JCR_ID)) - return false; - return true; - } - - } -} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsJcrFsProvider.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsJcrFsProvider.java new file mode 100644 index 000000000..0099b3bed --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsJcrFsProvider.java @@ -0,0 +1,133 @@ +package org.argeo.cms.jcr.internal; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.Path; +import java.nio.file.spi.FileSystemProvider; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.auth.CurrentUser; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.jackrabbit.fs.AbstractJackrabbitFsProvider; +import org.argeo.jcr.fs.JcrFileSystem; +import org.argeo.jcr.fs.JcrFileSystemProvider; +import org.argeo.jcr.fs.JcrFsException; + +/** Implementation of an {@link FileSystemProvider} based on Jackrabbit. */ +public class CmsJcrFsProvider extends AbstractJackrabbitFsProvider { + private Map fileSystems = new HashMap<>(); + + private RepositoryFactory repositoryFactory; + private Repository repository; + + @Override + public String getScheme() { + return CmsConstants.SCHEME_NODE; + } + + @Override + public FileSystem newFileSystem(URI uri, Map env) throws IOException { +// BundleContext bc = FrameworkUtil.getBundle(CmsJcrFsProvider.class).getBundleContext(); + String username = CurrentUser.getUsername(); + if (username == null) { + // TODO deal with anonymous + return null; + } + if (fileSystems.containsKey(username)) + throw new FileSystemAlreadyExistsException("CMS file system already exists for user " + username); + + try { + String host = uri.getHost(); + if (host != null && !host.trim().equals("")) { + URI repoUri = new URI("http", uri.getUserInfo(), uri.getHost(), uri.getPort(), "/jcr/node", null, null); +// RepositoryFactory repositoryFactory = bc.getService(bc.getServiceReference(RepositoryFactory.class)); + Repository repository = CmsJcrUtils.getRepositoryByUri(repositoryFactory, repoUri.toString()); + CmsFileSystem fileSystem = new CmsFileSystem(this, repository); + fileSystems.put(username, fileSystem); + return fileSystem; + } else { +// Repository repository = bc.getService( +// bc.getServiceReferences(Repository.class, "(cn=" + CmsConstants.EGO_REPOSITORY + ")") +// .iterator().next()); + + // Session session = repository.login(); + CmsFileSystem fileSystem = new CmsFileSystem(this, repository); + fileSystems.put(username, fileSystem); + return fileSystem; + } + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Cannot open file system " + uri + " for user " + username, e); + } + } + + @Override + public FileSystem getFileSystem(URI uri) { + return currentUserFileSystem(); + } + + @Override + public Path getPath(URI uri) { + JcrFileSystem fileSystem = currentUserFileSystem(); + String path = uri.getPath(); + if (fileSystem == null) + try { + fileSystem = (JcrFileSystem) newFileSystem(uri, new HashMap()); + } catch (IOException e) { + throw new JcrFsException("Could not autocreate file system", e); + } + return fileSystem.getPath(path); + } + + protected JcrFileSystem currentUserFileSystem() { + String username = CurrentUser.getUsername(); + return fileSystems.get(username); + } + + public Node getUserHome(Repository repository) { + try { + Session session = repository.login(CmsConstants.HOME_WORKSPACE); + return CmsJcrUtils.getUserHome(session); + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot get user home", e); + } + } + + public void setRepositoryFactory(RepositoryFactory repositoryFactory) { + this.repositoryFactory = repositoryFactory; + } + + public void setRepository(Repository repository) { + this.repository = repository; + } + + static class CmsFileSystem extends JcrFileSystem { + public CmsFileSystem(JcrFileSystemProvider provider, Repository repository) throws IOException { + super(provider, repository); + } + + public boolean skipNode(Node node) throws RepositoryException { +// if (node.isNodeType(NodeType.NT_HIERARCHY_NODE) || node.isNodeType(NodeTypes.NODE_USER_HOME) +// || node.isNodeType(NodeTypes.NODE_GROUP_HOME)) + if (node.isNodeType(NodeType.NT_HIERARCHY_NODE)) + return false; + // FIXME Better identifies home + if (node.hasProperty(Property.JCR_ID)) + return false; + return true; + } + + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/JcrRepositoryFactory.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/JcrRepositoryFactory.java new file mode 100644 index 000000000..342c1add7 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/JcrRepositoryFactory.java @@ -0,0 +1,191 @@ +package org.argeo.cms.jcr.internal; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; + +import org.apache.jackrabbit.jcr2dav.Jcr2davRepositoryFactory; +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.internal.jcr.RepoConf; +import org.argeo.cms.jcr.internal.osgi.CmsJcrActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; + +/** + * OSGi-aware Jackrabbit repository factory which can retrieve/publish + * {@link Repository} as OSGi services. + */ +public class JcrRepositoryFactory implements RepositoryFactory { + private final CmsLog log = CmsLog.getLog(getClass()); +// private final BundleContext bundleContext = FrameworkUtil.getBundle(getClass()).getBundleContext(); + + // private Resource fileRepositoryConfiguration = new ClassPathResource( + // "/org/argeo/cms/internal/kernel/repository-localfs.xml"); + + protected Repository getRepositoryByAlias(String alias) { + BundleContext bundleContext = CmsJcrActivator.getBundleContext(); + if (bundleContext != null) { + try { + Collection> srs = bundleContext.getServiceReferences(Repository.class, + "(" + CmsConstants.CN + "=" + alias + ")"); + if (srs.size() == 0) + throw new IllegalArgumentException("No repository with alias " + alias + " found in OSGi registry"); + else if (srs.size() > 1) + throw new IllegalArgumentException( + srs.size() + " repositories with alias " + alias + " found in OSGi registry"); + return bundleContext.getService(srs.iterator().next()); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("Cannot find repository with alias " + alias, e); + } + } else { + // TODO ability to filter static services + return null; + } + } + + // private void publish(String alias, Repository repository, Properties + // properties) { + // if (bundleContext != null) { + // // do not modify reference + // Hashtable props = new Hashtable(); + // props.putAll(props); + // props.put(JCR_REPOSITORY_ALIAS, alias); + // bundleContext.registerService(Repository.class.getName(), repository, + // props); + // } + // } + + @SuppressWarnings({ "rawtypes" }) + public Repository getRepository(Map parameters) throws RepositoryException { + // // check if can be found by alias + // Repository repository = super.getRepository(parameters); + // if (repository != null) + // return repository; + + // check if remote + Repository repository; + String uri = null; + if (parameters.containsKey(RepoConf.labeledUri.name())) + uri = parameters.get(CmsConstants.LABELED_URI).toString(); + else if (parameters.containsKey(KernelConstants.JACKRABBIT_REPOSITORY_URI)) + uri = parameters.get(KernelConstants.JACKRABBIT_REPOSITORY_URI).toString(); + + if (uri != null) { + if (uri.startsWith("http")) {// http, https + Object defaultWorkspace = parameters.get(RepoConf.defaultWorkspace.name()); + repository = createRemoteRepository(uri, defaultWorkspace != null ? defaultWorkspace.toString() : null); + } else if (uri.startsWith("file"))// http, https + repository = createFileRepository(uri, parameters); + else if (uri.startsWith("vm")) { + // log.warn("URI " + uri + " should have been managed by generic + // JCR repository factory"); + repository = getRepositoryByAlias(getAliasFromURI(uri)); + } else + throw new IllegalArgumentException("Unrecognized URI format " + uri); + + } + + else if (parameters.containsKey(CmsConstants.CN)) { + // Properties properties = new Properties(); + // properties.putAll(parameters); + String alias = parameters.get(CmsConstants.CN).toString(); + // publish(alias, repository, properties); + // log.info("Registered JCR repository under alias '" + alias + "' + // with properties " + properties); + repository = getRepositoryByAlias(alias); + } else + throw new IllegalArgumentException("Not enough information in " + parameters); + + if (repository == null) + throw new IllegalArgumentException("Repository not found " + parameters); + + return repository; + } + + protected Repository createRemoteRepository(String uri, String defaultWorkspace) throws RepositoryException { + Map params = new HashMap(); + params.put(KernelConstants.JACKRABBIT_REPOSITORY_URI, uri); + if (defaultWorkspace != null) + params.put(KernelConstants.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, defaultWorkspace); + Repository repository = new Jcr2davRepositoryFactory().getRepository(params); + if (repository == null) + throw new IllegalArgumentException("Remote Davex repository " + uri + " not found"); + log.info("Initialized remote Jackrabbit repository from uri " + uri); + return repository; + } + + @SuppressWarnings({ "rawtypes" }) + protected Repository createFileRepository(final String uri, Map parameters) throws RepositoryException { + throw new UnsupportedOperationException(); + // InputStream configurationIn = null; + // try { + // Properties vars = new Properties(); + // vars.putAll(parameters); + // String dirPath = uri.substring("file:".length()); + // File homeDir = new File(dirPath); + // if (homeDir.exists() && !homeDir.isDirectory()) + // throw new ArgeoJcrException("Repository home " + dirPath + " is not a + // directory"); + // if (!homeDir.exists()) + // homeDir.mkdirs(); + // configurationIn = fileRepositoryConfiguration.getInputStream(); + // vars.put(RepositoryConfigurationParser.REPOSITORY_HOME_VARIABLE, + // homeDir.getCanonicalPath()); + // RepositoryConfig repositoryConfig = RepositoryConfig.create(new + // InputSource(configurationIn), vars); + // + // // TransientRepository repository = new + // // TransientRepository(repositoryConfig); + // final RepositoryImpl repository = + // RepositoryImpl.create(repositoryConfig); + // Session session = repository.login(); + // // FIXME make it generic + // org.argeo.jcr.JcrUtils.addPrivilege(session, "/", "ROLE_ADMIN", + // "jcr:all"); + // org.argeo.jcr.JcrUtils.logoutQuietly(session); + // Runtime.getRuntime().addShutdownHook(new Thread("Clean JCR repository + // " + uri) { + // public void run() { + // repository.shutdown(); + // log.info("Destroyed repository " + uri); + // } + // }); + // log.info("Initialized file Jackrabbit repository from uri " + uri); + // return repository; + // } catch (Exception e) { + // throw new ArgeoJcrException("Cannot create repository " + uri, e); + // } finally { + // IOUtils.closeQuietly(configurationIn); + // } + } + + protected String getAliasFromURI(String uri) { + try { + URI uriObj = new URI(uri); + String alias = uriObj.getPath(); + if (alias.charAt(0) == '/') + alias = alias.substring(1); + if (alias.charAt(alias.length() - 1) == '/') + alias = alias.substring(0, alias.length() - 1); + return alias; + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Cannot interpret URI " + uri, e); + } + } + + /** + * Called after the repository has been initialised. Does nothing by default. + */ + @SuppressWarnings("rawtypes") + protected void postInitialization(Repository repository, Map parameters) { + + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/NodeRepositoryFactory.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/NodeRepositoryFactory.java deleted file mode 100644 index a4ab7cb23..000000000 --- a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/NodeRepositoryFactory.java +++ /dev/null @@ -1,191 +0,0 @@ -package org.argeo.cms.jcr.internal; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.RepositoryFactory; - -import org.apache.jackrabbit.jcr2dav.Jcr2davRepositoryFactory; -import org.argeo.api.cms.CmsConstants; -import org.argeo.api.cms.CmsLog; -import org.argeo.cms.internal.jcr.RepoConf; -import org.argeo.cms.jcr.internal.osgi.CmsJcrActivator; -import org.osgi.framework.BundleContext; -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.framework.ServiceReference; - -/** - * OSGi-aware Jackrabbit repository factory which can retrieve/publish - * {@link Repository} as OSGi services. - */ -public class NodeRepositoryFactory implements RepositoryFactory { - private final CmsLog log = CmsLog.getLog(getClass()); -// private final BundleContext bundleContext = FrameworkUtil.getBundle(getClass()).getBundleContext(); - - // private Resource fileRepositoryConfiguration = new ClassPathResource( - // "/org/argeo/cms/internal/kernel/repository-localfs.xml"); - - protected Repository getRepositoryByAlias(String alias) { - BundleContext bundleContext = CmsJcrActivator.getBundleContext(); - if (bundleContext != null) { - try { - Collection> srs = bundleContext.getServiceReferences(Repository.class, - "(" + CmsConstants.CN + "=" + alias + ")"); - if (srs.size() == 0) - throw new IllegalArgumentException("No repository with alias " + alias + " found in OSGi registry"); - else if (srs.size() > 1) - throw new IllegalArgumentException( - srs.size() + " repositories with alias " + alias + " found in OSGi registry"); - return bundleContext.getService(srs.iterator().next()); - } catch (InvalidSyntaxException e) { - throw new IllegalArgumentException("Cannot find repository with alias " + alias, e); - } - } else { - // TODO ability to filter static services - return null; - } - } - - // private void publish(String alias, Repository repository, Properties - // properties) { - // if (bundleContext != null) { - // // do not modify reference - // Hashtable props = new Hashtable(); - // props.putAll(props); - // props.put(JCR_REPOSITORY_ALIAS, alias); - // bundleContext.registerService(Repository.class.getName(), repository, - // props); - // } - // } - - @SuppressWarnings({ "rawtypes" }) - public Repository getRepository(Map parameters) throws RepositoryException { - // // check if can be found by alias - // Repository repository = super.getRepository(parameters); - // if (repository != null) - // return repository; - - // check if remote - Repository repository; - String uri = null; - if (parameters.containsKey(RepoConf.labeledUri.name())) - uri = parameters.get(CmsConstants.LABELED_URI).toString(); - else if (parameters.containsKey(KernelConstants.JACKRABBIT_REPOSITORY_URI)) - uri = parameters.get(KernelConstants.JACKRABBIT_REPOSITORY_URI).toString(); - - if (uri != null) { - if (uri.startsWith("http")) {// http, https - Object defaultWorkspace = parameters.get(RepoConf.defaultWorkspace.name()); - repository = createRemoteRepository(uri, defaultWorkspace != null ? defaultWorkspace.toString() : null); - } else if (uri.startsWith("file"))// http, https - repository = createFileRepository(uri, parameters); - else if (uri.startsWith("vm")) { - // log.warn("URI " + uri + " should have been managed by generic - // JCR repository factory"); - repository = getRepositoryByAlias(getAliasFromURI(uri)); - } else - throw new IllegalArgumentException("Unrecognized URI format " + uri); - - } - - else if (parameters.containsKey(CmsConstants.CN)) { - // Properties properties = new Properties(); - // properties.putAll(parameters); - String alias = parameters.get(CmsConstants.CN).toString(); - // publish(alias, repository, properties); - // log.info("Registered JCR repository under alias '" + alias + "' - // with properties " + properties); - repository = getRepositoryByAlias(alias); - } else - throw new IllegalArgumentException("Not enough information in " + parameters); - - if (repository == null) - throw new IllegalArgumentException("Repository not found " + parameters); - - return repository; - } - - protected Repository createRemoteRepository(String uri, String defaultWorkspace) throws RepositoryException { - Map params = new HashMap(); - params.put(KernelConstants.JACKRABBIT_REPOSITORY_URI, uri); - if (defaultWorkspace != null) - params.put(KernelConstants.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, defaultWorkspace); - Repository repository = new Jcr2davRepositoryFactory().getRepository(params); - if (repository == null) - throw new IllegalArgumentException("Remote Davex repository " + uri + " not found"); - log.info("Initialized remote Jackrabbit repository from uri " + uri); - return repository; - } - - @SuppressWarnings({ "rawtypes" }) - protected Repository createFileRepository(final String uri, Map parameters) throws RepositoryException { - throw new UnsupportedOperationException(); - // InputStream configurationIn = null; - // try { - // Properties vars = new Properties(); - // vars.putAll(parameters); - // String dirPath = uri.substring("file:".length()); - // File homeDir = new File(dirPath); - // if (homeDir.exists() && !homeDir.isDirectory()) - // throw new ArgeoJcrException("Repository home " + dirPath + " is not a - // directory"); - // if (!homeDir.exists()) - // homeDir.mkdirs(); - // configurationIn = fileRepositoryConfiguration.getInputStream(); - // vars.put(RepositoryConfigurationParser.REPOSITORY_HOME_VARIABLE, - // homeDir.getCanonicalPath()); - // RepositoryConfig repositoryConfig = RepositoryConfig.create(new - // InputSource(configurationIn), vars); - // - // // TransientRepository repository = new - // // TransientRepository(repositoryConfig); - // final RepositoryImpl repository = - // RepositoryImpl.create(repositoryConfig); - // Session session = repository.login(); - // // FIXME make it generic - // org.argeo.jcr.JcrUtils.addPrivilege(session, "/", "ROLE_ADMIN", - // "jcr:all"); - // org.argeo.jcr.JcrUtils.logoutQuietly(session); - // Runtime.getRuntime().addShutdownHook(new Thread("Clean JCR repository - // " + uri) { - // public void run() { - // repository.shutdown(); - // log.info("Destroyed repository " + uri); - // } - // }); - // log.info("Initialized file Jackrabbit repository from uri " + uri); - // return repository; - // } catch (Exception e) { - // throw new ArgeoJcrException("Cannot create repository " + uri, e); - // } finally { - // IOUtils.closeQuietly(configurationIn); - // } - } - - protected String getAliasFromURI(String uri) { - try { - URI uriObj = new URI(uri); - String alias = uriObj.getPath(); - if (alias.charAt(0) == '/') - alias = alias.substring(1); - if (alias.charAt(alias.length() - 1) == '/') - alias = alias.substring(0, alias.length() - 1); - return alias; - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Cannot interpret URI " + uri, e); - } - } - - /** - * Called after the repository has been initialised. Does nothing by default. - */ - @SuppressWarnings("rawtypes") - protected void postInitialization(Repository repository, Map parameters) { - - } -} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/RepositoryContextsFactory.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/RepositoryContextsFactory.java new file mode 100644 index 000000000..1a1fda1f0 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/RepositoryContextsFactory.java @@ -0,0 +1,136 @@ +package org.argeo.cms.jcr.internal; + +import java.net.URI; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.Repository; +import javax.jcr.RepositoryFactory; + +import org.apache.jackrabbit.core.RepositoryContext; +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.internal.jcr.RepoConf; +import org.argeo.cms.internal.jcr.RepositoryBuilder; +import org.argeo.cms.jcr.internal.osgi.CmsJcrActivator; +import org.argeo.util.LangUtils; +import org.osgi.framework.Constants; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedServiceFactory; + +/** A {@link ManagedServiceFactory} creating or referencing JCR repositories. */ +public class RepositoryContextsFactory implements ManagedServiceFactory { + private final static CmsLog log = CmsLog.getLog(RepositoryContextsFactory.class); +// private final BundleContext bc = FrameworkUtil.getBundle(RepositoryServiceFactory.class).getBundleContext(); + + private Map repositories = new HashMap(); + private Map pidToCn = new HashMap(); + + public void init() { + + } + + public void destroy() { + for (String pid : repositories.keySet()) { + try { + repositories.get(pid).getRepository().shutdown(); + if (log.isDebugEnabled()) + log.debug("Shut down repository " + pid + + (pidToCn.containsKey(pid) ? " (" + pidToCn.get(pid) + ")" : "")); + } catch (Exception e) { + log.error("Error when shutting down Jackrabbit repository " + pid, e); + } + } + } + + + @Override + public String getName() { + return "Jackrabbit repository service factory"; + } + + @Override + public void updated(String pid, Dictionary properties) throws ConfigurationException { + if (repositories.containsKey(pid)) + throw new IllegalArgumentException("Already a repository registered for " + pid); + + if (properties == null) + return; + + if (repositories.containsKey(pid)) { + log.warn("Ignore update of Jackrabbit repository " + pid); + return; + } + + try { + Object labeledUri = properties.get(RepoConf.labeledUri.name()); + if (labeledUri == null) { + RepositoryBuilder repositoryBuilder = new RepositoryBuilder(); + RepositoryContext repositoryContext = repositoryBuilder.createRepositoryContext(properties); + repositories.put(pid, repositoryContext); + Dictionary props = LangUtils.dict(Constants.SERVICE_PID, pid); + // props.put(ArgeoJcrConstants.JCR_REPOSITORY_URI, + // properties.get(RepoConf.labeledUri.name())); + Object cn = properties.get(CmsConstants.CN); + if (cn != null) { + props.put(CmsConstants.CN, cn); + // props.put(NodeConstants.JCR_REPOSITORY_ALIAS, cn); + pidToCn.put(pid, cn); + } + CmsJcrActivator.registerService(RepositoryContext.class, repositoryContext, props); + } else { + try { + Object cn = properties.get(CmsConstants.CN); + Object defaultWorkspace = properties.get(RepoConf.defaultWorkspace.name()); + if (defaultWorkspace == null) + defaultWorkspace = RepoConf.defaultWorkspace.getDefault(); + URI uri = new URI(labeledUri.toString()); +// RepositoryFactory repositoryFactory = bc +// .getService(bc.getServiceReference(RepositoryFactory.class)); + RepositoryFactory repositoryFactory = CmsJcrActivator.getService(RepositoryFactory.class); + Map parameters = new HashMap(); + parameters.put(RepoConf.labeledUri.name(), uri.toString()); + parameters.put(RepoConf.defaultWorkspace.name(), defaultWorkspace.toString()); + Repository repository = repositoryFactory.getRepository(parameters); + // Repository repository = NodeUtils.getRepositoryByUri(repositoryFactory, + // uri.toString()); + Dictionary props = LangUtils.dict(Constants.SERVICE_PID, pid); + props.put(RepoConf.labeledUri.name(), + new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), null, null) + .toString()); + if (cn != null) { + props.put(CmsConstants.CN, cn); + // props.put(NodeConstants.JCR_REPOSITORY_ALIAS, cn); + pidToCn.put(pid, cn); + } + CmsJcrActivator.registerService(Repository.class, repository, props); + + // home + if (cn.equals(CmsConstants.NODE_REPOSITORY)) { + Dictionary homeProps = LangUtils.dict(CmsConstants.CN, + CmsConstants.EGO_REPOSITORY); + EgoRepository homeRepository = new EgoRepository(repository, true); + CmsJcrActivator.registerService(Repository.class, homeRepository, homeProps); + } + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } catch (Exception e) { + throw new IllegalStateException("Cannot create Jackrabbit repository " + pid, e); + } + + } + + @Override + public void deleted(String pid) { + RepositoryContext repositoryContext = repositories.remove(pid); + repositoryContext.getRepository().shutdown(); + if (log.isDebugEnabled()) + log.debug("Deleted repository " + pid); + } + + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/RepositoryServiceFactory.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/RepositoryServiceFactory.java deleted file mode 100644 index 042ddb5d5..000000000 --- a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/RepositoryServiceFactory.java +++ /dev/null @@ -1,130 +0,0 @@ -package org.argeo.cms.jcr.internal; - -import java.net.URI; -import java.util.Dictionary; -import java.util.HashMap; -import java.util.Map; - -import javax.jcr.Repository; -import javax.jcr.RepositoryFactory; - -import org.apache.jackrabbit.core.RepositoryContext; -import org.argeo.api.cms.CmsConstants; -import org.argeo.api.cms.CmsLog; -import org.argeo.cms.internal.jcr.RepoConf; -import org.argeo.cms.internal.jcr.RepositoryBuilder; -import org.argeo.cms.jcr.internal.osgi.CmsJcrActivator; -import org.argeo.util.LangUtils; -import org.osgi.framework.Constants; -import org.osgi.service.cm.ConfigurationException; -import org.osgi.service.cm.ManagedServiceFactory; - -/** A {@link ManagedServiceFactory} creating or referencing JCR repositories. */ -public class RepositoryServiceFactory implements ManagedServiceFactory { - private final static CmsLog log = CmsLog.getLog(RepositoryServiceFactory.class); -// private final BundleContext bc = FrameworkUtil.getBundle(RepositoryServiceFactory.class).getBundleContext(); - - private Map repositories = new HashMap(); - private Map pidToCn = new HashMap(); - - @Override - public String getName() { - return "Jackrabbit repository service factory"; - } - - @Override - public void updated(String pid, Dictionary properties) throws ConfigurationException { - if (repositories.containsKey(pid)) - throw new IllegalArgumentException("Already a repository registered for " + pid); - - if (properties == null) - return; - - if (repositories.containsKey(pid)) { - log.warn("Ignore update of Jackrabbit repository " + pid); - return; - } - - try { - Object labeledUri = properties.get(RepoConf.labeledUri.name()); - if (labeledUri == null) { - RepositoryBuilder repositoryBuilder = new RepositoryBuilder(); - RepositoryContext repositoryContext = repositoryBuilder.createRepositoryContext(properties); - repositories.put(pid, repositoryContext); - Dictionary props = LangUtils.dict(Constants.SERVICE_PID, pid); - // props.put(ArgeoJcrConstants.JCR_REPOSITORY_URI, - // properties.get(RepoConf.labeledUri.name())); - Object cn = properties.get(CmsConstants.CN); - if (cn != null) { - props.put(CmsConstants.CN, cn); - // props.put(NodeConstants.JCR_REPOSITORY_ALIAS, cn); - pidToCn.put(pid, cn); - } - CmsJcrActivator.registerService(RepositoryContext.class, repositoryContext, props); - } else { - try { - Object cn = properties.get(CmsConstants.CN); - Object defaultWorkspace = properties.get(RepoConf.defaultWorkspace.name()); - if (defaultWorkspace == null) - defaultWorkspace = RepoConf.defaultWorkspace.getDefault(); - URI uri = new URI(labeledUri.toString()); -// RepositoryFactory repositoryFactory = bc -// .getService(bc.getServiceReference(RepositoryFactory.class)); - RepositoryFactory repositoryFactory = CmsJcrActivator.getService(RepositoryFactory.class); - Map parameters = new HashMap(); - parameters.put(RepoConf.labeledUri.name(), uri.toString()); - parameters.put(RepoConf.defaultWorkspace.name(), defaultWorkspace.toString()); - Repository repository = repositoryFactory.getRepository(parameters); - // Repository repository = NodeUtils.getRepositoryByUri(repositoryFactory, - // uri.toString()); - Dictionary props = LangUtils.dict(Constants.SERVICE_PID, pid); - props.put(RepoConf.labeledUri.name(), - new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), null, null) - .toString()); - if (cn != null) { - props.put(CmsConstants.CN, cn); - // props.put(NodeConstants.JCR_REPOSITORY_ALIAS, cn); - pidToCn.put(pid, cn); - } - CmsJcrActivator.registerService(Repository.class, repository, props); - - // home - if (cn.equals(CmsConstants.NODE_REPOSITORY)) { - Dictionary homeProps = LangUtils.dict(CmsConstants.CN, - CmsConstants.EGO_REPOSITORY); - EgoRepository homeRepository = new EgoRepository(repository, true); - CmsJcrActivator.registerService(Repository.class, homeRepository, homeProps); - } - } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - } catch (Exception e) { - throw new IllegalStateException("Cannot create Jackrabbit repository " + pid, e); - } - - } - - @Override - public void deleted(String pid) { - RepositoryContext repositoryContext = repositories.remove(pid); - repositoryContext.getRepository().shutdown(); - if (log.isDebugEnabled()) - log.debug("Deleted repository " + pid); - } - - public void shutdown() { - for (String pid : repositories.keySet()) { - try { - repositories.get(pid).getRepository().shutdown(); - if (log.isDebugEnabled()) - log.debug("Shut down repository " + pid - + (pidToCn.containsKey(pid) ? " (" + pidToCn.get(pid) + ")" : "")); - } catch (Exception e) { - log.error("Error when shutting down Jackrabbit repository " + pid, e); - } - } - } - -} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/osgi/CmsJcrActivator.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/osgi/CmsJcrActivator.java index 7332fde29..57860d84f 100644 --- a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/osgi/CmsJcrActivator.java +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/osgi/CmsJcrActivator.java @@ -1,20 +1,10 @@ package org.argeo.cms.jcr.internal.osgi; -import java.nio.file.spi.FileSystemProvider; import java.util.Dictionary; -import javax.jcr.RepositoryFactory; - -import org.argeo.api.cms.CmsConstants; -import org.argeo.cms.jcr.internal.CmsFsProvider; -import org.argeo.cms.jcr.internal.NodeRepositoryFactory; -import org.argeo.cms.jcr.internal.RepositoryServiceFactory; import org.argeo.cms.jcr.internal.StatisticsThread; -import org.argeo.util.LangUtils; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; -import org.osgi.framework.Constants; -import org.osgi.service.cm.ManagedServiceFactory; public class CmsJcrActivator implements BundleActivator { private static BundleContext bundleContext; @@ -22,7 +12,7 @@ public class CmsJcrActivator implements BundleActivator { // private List stopHooks = new ArrayList<>(); private StatisticsThread kernelThread; - private RepositoryServiceFactory repositoryServiceFactory; +// private JackrabbitRepositoryContextsFactory repositoryServiceFactory; // private CmsJcrDeployment jcrDeployment; @Override @@ -35,16 +25,16 @@ public class CmsJcrActivator implements BundleActivator { kernelThread.start(); // JCR - repositoryServiceFactory = new RepositoryServiceFactory(); -// stopHooks.add(() -> repositoryServiceFactory.shutdown()); - registerService(ManagedServiceFactory.class, repositoryServiceFactory, - LangUtils.dict(Constants.SERVICE_PID, CmsConstants.NODE_REPOS_FACTORY_PID)); +// repositoryServiceFactory = new JackrabbitRepositoryContextsFactory(); +//// stopHooks.add(() -> repositoryServiceFactory.shutdown()); +// registerService(ManagedServiceFactory.class, repositoryServiceFactory, +// LangUtils.dict(Constants.SERVICE_PID, CmsConstants.NODE_REPOS_FACTORY_PID)); - NodeRepositoryFactory repositoryFactory = new NodeRepositoryFactory(); - registerService(RepositoryFactory.class, repositoryFactory, null); +// JcrRepositoryFactory repositoryFactory = new JcrRepositoryFactory(); +// registerService(RepositoryFactory.class, repositoryFactory, null); // File System - CmsFsProvider cmsFsProvider = new CmsFsProvider(); +// CmsJcrFsProvider cmsFsProvider = new CmsJcrFsProvider(); // ServiceLoader fspSl = ServiceLoader.load(FileSystemProvider.class); // for (FileSystemProvider fsp : fspSl) { // log.debug("FileSystemProvider " + fsp); @@ -55,8 +45,8 @@ public class CmsJcrActivator implements BundleActivator { // for (FileSystemProvider fsp : FileSystemProvider.installedProviders()) { // log.debug("Installed FileSystemProvider " + fsp); // } - registerService(FileSystemProvider.class, cmsFsProvider, - LangUtils.dict(Constants.SERVICE_PID, CmsConstants.NODE_FS_PROVIDER_PID)); +// registerService(FileSystemProvider.class, cmsFsProvider, +// LangUtils.dict(Constants.SERVICE_PID, CmsConstants.NODE_FS_PROVIDER_PID)); // jcrDeployment = new CmsJcrDeployment(); // jcrDeployment.init(); @@ -67,8 +57,8 @@ public class CmsJcrActivator implements BundleActivator { // if (jcrDeployment != null) // jcrDeployment.destroy(); - if (repositoryServiceFactory != null) - repositoryServiceFactory.shutdown(); +// if (repositoryServiceFactory != null) +// repositoryServiceFactory.shutdown(); if (kernelThread != null) kernelThread.destroyAndJoin(); diff --git a/org.argeo.cms/OSGI-INF/deployConfig.xml b/org.argeo.cms/OSGI-INF/deployConfig.xml index 10fcb547f..85b309021 100644 --- a/org.argeo.cms/OSGI-INF/deployConfig.xml +++ b/org.argeo.cms/OSGI-INF/deployConfig.xml @@ -5,4 +5,5 @@ + diff --git a/org.argeo.cms/OSGI-INF/nodeUserAdmin.xml b/org.argeo.cms/OSGI-INF/nodeUserAdmin.xml index 7becce748..f86eba632 100644 --- a/org.argeo.cms/OSGI-INF/nodeUserAdmin.xml +++ b/org.argeo.cms/OSGI-INF/nodeUserAdmin.xml @@ -7,4 +7,5 @@ + diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java index 7b47b520d..cf203e5e6 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java @@ -28,7 +28,6 @@ public class CmsStateImpl implements CmsState { // REFERENCES private Long availableSince; - // private ThreadGroup threadGroup = new ThreadGroup("CMS"); private List stopHooks = new ArrayList<>(); @@ -49,27 +48,27 @@ public class CmsStateImpl implements CmsState { if (log.isTraceEnabled()) log.trace("CMS State started"); - } catch (Throwable e) { - log.error("## FATAL: CMS activator failed", e); - } - this.stateUuid = KernelUtils.getFrameworkProp(Constants.FRAMEWORK_UUID); + this.stateUuid = KernelUtils.getFrameworkProp(Constants.FRAMEWORK_UUID); // this.cleanState = stateUuid.equals(frameworkUuid); - try { - this.hostname = InetAddress.getLocalHost().getHostName(); - } catch (UnknownHostException e) { - log.error("Cannot set hostname: " + e); - } + try { + this.hostname = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + log.error("Cannot set hostname: " + e); + } - availableSince = System.currentTimeMillis(); - if (log.isDebugEnabled()) - // log.debug("## CMS starting... stateUuid=" + this.stateUuid + (cleanState ? " - // (clean state) " : " ")); - log.debug("## CMS starting... (" + stateUuid + ")"); + availableSince = System.currentTimeMillis(); + if (log.isDebugEnabled()) + // log.debug("## CMS starting... stateUuid=" + this.stateUuid + (cleanState ? " + // (clean state) " : " ")); + log.debug("## CMS starting... (" + stateUuid + ")"); // initI18n(); // initServices(); + } catch (RuntimeException e) { + log.error("## FATAL: CMS activator failed", e); + } } private void initSecurity() { diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/KernelConstants.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/KernelConstants.java index 7c397de6c..d491a6623 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/KernelConstants.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/KernelConstants.java @@ -18,8 +18,8 @@ public interface KernelConstants { String NODE_KEY_TAB_PATH = DIR_NODE + "/krb5.keytab"; // Security - String JAAS_CONFIG = "/org/argeo/cms/internal/kernel/jaas.cfg"; - String JAAS_CONFIG_IPA = "/org/argeo/cms/internal/kernel/jaas-ipa.cfg"; + String JAAS_CONFIG = "/org/argeo/cms/internal/runtime/jaas.cfg"; + String JAAS_CONFIG_IPA = "/org/argeo/cms/internal/runtime/jaas-ipa.cfg"; // Java String JAAS_CONFIG_PROP = "java.security.auth.login.config"; diff --git a/org.argeo.init/src/org/argeo/init/logging/ThinJavaUtilLogging.java b/org.argeo.init/src/org/argeo/init/logging/ThinJavaUtilLogging.java index f1143d670..1bfab8e92 100644 --- a/org.argeo.init/src/org/argeo/init/logging/ThinJavaUtilLogging.java +++ b/org.argeo.init/src/org/argeo/init/logging/ThinJavaUtilLogging.java @@ -91,7 +91,8 @@ class ThinJavaUtilLogging { @Override public void publish(LogRecord record) { java.lang.System.Logger systemLogger = ThinLoggerFinder.getLogger(record.getLoggerName()); - systemLogger.log(ThinJavaUtilLogging.fromJulLevel(record.getLevel()), record.getMessage()); + systemLogger.log(ThinJavaUtilLogging.fromJulLevel(record.getLevel()), record.getMessage(), + record.getThrown()); } @Override diff --git a/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java b/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java index 9866a1f23..871dc8d7f 100644 --- a/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java +++ b/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java @@ -216,7 +216,7 @@ class ThinLogging implements Consumer> { /* * INTERNAL CLASSES */ - + private class ThinLogger implements System.Logger { private final String name; @@ -297,9 +297,10 @@ class ThinLogging implements Consumer> { case "java.util.logging.Logger": case "org.apache.commons.logging.Log": case "org.osgi.service.log.Logger": - case "org.argeo.cms.Log": + case "org.argeo.api.cms.CmsLog": case "org.slf4j.impl.ArgeoLogger": case "org.eclipse.jetty.util.log.Slf4jLog": + case "sun.util.logging.internal.LoggingProviderImpl$JULWrapper": lowestLoggerInterface = i; continue stack; default: @@ -519,12 +520,13 @@ class ThinLogging implements Consumer> { sb.append('\n'); for (StackTraceElement ste : throwable.getStackTrace()) { sb.append(prefix); + sb.append('\t'); sb.append(ste.toString()); sb.append('\n'); } if (throwable.getCause() != null) { sb.append(prefix); - sb.append("caused by "); + sb.append("Caused by: "); addThrowable(sb, prefix, throwable.getCause()); } }