From e890c9d9d57069b464b406405797f38e7263f3b1 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Mon, 6 Jun 2011 15:55:32 +0000 Subject: [PATCH] Introduce Jackrabbit data model migration git-svn-id: https://svn.argeo.org/commons/trunk@4560 4cfe0d0a-d680-48aa-b62c-e0a02a3f76cc --- .../jackrabbit/JackrabbitAuthorizations.java | 10 +- .../argeo/jackrabbit/JackrabbitContainer.java | 242 +++++++++++------- .../JackrabbitDataModelMigration.java | 150 +++++++++++ .../main/java/org/argeo/jcr/ArgeoNames.java | 1 + .../main/java/org/argeo/jcr/JcrCallback.java | 9 + .../src/main/java/org/argeo/jcr/JcrUtils.java | 11 +- 6 files changed, 320 insertions(+), 103 deletions(-) create mode 100644 server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitDataModelMigration.java create mode 100644 server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrCallback.java diff --git a/server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitAuthorizations.java b/server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitAuthorizations.java index ee0312164..8c1204c0e 100644 --- a/server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitAuthorizations.java +++ b/server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitAuthorizations.java @@ -22,6 +22,7 @@ import org.apache.jackrabbit.api.security.user.UserManager; import org.argeo.ArgeoException; import org.argeo.jcr.JcrUtils; +/** Apply authorizations to a Jackrabbit repository. */ public class JackrabbitAuthorizations { private final static Log log = LogFactory .getLog(JackrabbitAuthorizations.class); @@ -36,7 +37,7 @@ public class JackrabbitAuthorizations { private Map groupPrivileges = new HashMap(); public void init() { - systemExecutor.execute(new Runnable() { + Runnable action = new Runnable() { public void run() { JackrabbitSession session = null; try { @@ -48,7 +49,12 @@ public class JackrabbitAuthorizations { JcrUtils.logoutQuietly(session); } } - }); + }; + + if (systemExecutor != null) + systemExecutor.execute(action); + else + action.run(); } protected void initAuthorizations(JackrabbitSession session) diff --git a/server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitContainer.java b/server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitContainer.java index 294fdd5a3..e27a473b5 100644 --- a/server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitContainer.java +++ b/server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitContainer.java @@ -24,9 +24,12 @@ import java.io.InputStreamReader; import java.io.Reader; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.Executor; import javax.jcr.Credentials; @@ -51,8 +54,6 @@ import org.apache.jackrabbit.core.config.RepositoryConfigurationParser; import org.apache.jackrabbit.jcr2dav.Jcr2davRepositoryFactory; import org.argeo.ArgeoException; import org.argeo.jcr.JcrUtils; -import org.springframework.beans.factory.DisposableBean; -import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; @@ -63,13 +64,14 @@ import org.xml.sax.InputSource; * Wrapper around a Jackrabbit repository which allows to configure it in Spring * and expose it as a {@link Repository}. */ -public class JackrabbitContainer implements InitializingBean, DisposableBean, - Repository, ResourceLoaderAware { +public class JackrabbitContainer implements Repository, ResourceLoaderAware { private Log log = LogFactory.getLog(JackrabbitContainer.class); private Resource configuration; - private File homeDirectory; + private String homeDirectory; private Resource variables; + /** cache home */ + private File home; private Boolean inMemory = false; private String uri = null; @@ -81,104 +83,147 @@ public class JackrabbitContainer implements InitializingBean, DisposableBean, /** Node type definitions in CND format */ private List cndFiles = new ArrayList(); + /** Migrations to execute (if not already done) */ + private Set dataModelMigrations = new HashSet(); + /** Namespaces to register: key is prefix, value namespace */ private Map namespaces = new HashMap(); private Boolean autocreateWorkspaces = false; private Executor systemExecutor; - private Credentials adminCredentials; - - // transition from legacy spring approach - private Boolean alreadyInitialized = false; - private Boolean alreadyDisposed = false; - - /** @deprecated explicitly declare {@link #init()} as init-method instead. */ - public void afterPropertiesSet() throws Exception { - if (!alreadyInitialized) { - log.warn("## If not already done," - + " declare init-method=\"init\"."); - initImpl(); - } - } public void init() throws Exception { - initImpl(); - alreadyInitialized = true; - } - - protected void initImpl() throws Exception { if (repository != null) { // we are just wrapping another repository importNodeTypeDefinitions(repository); return; } - // remote repository - if (uri != null && !uri.trim().equals("")) { - Map params = new HashMap(); - params.put(org.apache.jackrabbit.commons.JcrUtils.REPOSITORY_URI, - uri); - repository = new Jcr2davRepositoryFactory().getRepository(params); - if (repository == null) - throw new ArgeoException("Remote Davex repository " + uri - + " not found"); - log.info("Initialized Jackrabbit repository " + repository - + " from URI " + uri); - // do not perform further initialization since we assume that the - // remote repository has been properly configured - return; - } + repository = createJackrabbitRepository(); - // local repository - if (inMemory && homeDirectory.exists()) { - FileUtils.deleteDirectory(homeDirectory); - log.warn("Deleted Jackrabbit home directory " + homeDirectory); - } + // migrate if needed + migrate(); + + // apply new CND files after migration + if (cndFiles != null && cndFiles.size() > 0) + importNodeTypeDefinitions(repository); + } - RepositoryConfig config; - Properties vars = getConfigurationProperties(); - InputStream in = configuration.getInputStream(); + /** Actually creates a new repository. */ + protected JackrabbitRepository createJackrabbitRepository() { + JackrabbitRepository repository; try { - vars.put(RepositoryConfigurationParser.REPOSITORY_HOME_VARIABLE, - homeDirectory.getCanonicalPath()); - config = RepositoryConfig.create(new InputSource(in), vars); + // remote repository + if (uri != null && !uri.trim().equals("")) { + Map params = new HashMap(); + params.put( + org.apache.jackrabbit.commons.JcrUtils.REPOSITORY_URI, + uri); + repository = (JackrabbitRepository) new Jcr2davRepositoryFactory() + .getRepository(params); + if (repository == null) + throw new ArgeoException("Remote Davex repository " + uri + + " not found"); + log.info("Initialized Jackrabbit repository " + repository + + " from URI " + uri); + // do not perform further initialization since we assume that + // the + // remote repository has been properly configured + return repository; + } + + // local repository + if (inMemory && getHome().exists()) { + FileUtils.deleteDirectory(getHome()); + log.warn("Deleted Jackrabbit home directory " + getHome()); + } + + RepositoryConfig config; + Properties vars = getConfigurationProperties(); + InputStream in = configuration.getInputStream(); + try { + vars.put( + RepositoryConfigurationParser.REPOSITORY_HOME_VARIABLE, + getHome().getCanonicalPath()); + config = RepositoryConfig.create(new InputSource(in), vars); + } catch (Exception e) { + throw new RuntimeException("Cannot read configuration", e); + } finally { + IOUtils.closeQuietly(in); + } + + if (inMemory) + repository = new TransientRepository(config); + else + repository = RepositoryImpl.create(config); + + log.info("Initialized Jackrabbit repository " + repository + " in " + + getHome() + " with config " + configuration); + + return repository; } catch (Exception e) { - throw new RuntimeException("Cannot read configuration", e); - } finally { - IOUtils.closeQuietly(in); + throw new ArgeoException("Cannot create Jackrabbit repository " + + getHome(), e); } + } - if (inMemory) - repository = new TransientRepository(config); - else - repository = RepositoryImpl.create(config); + /** Executes migrations, if needed. */ + protected void migrate() { + Boolean restartAndClearCaches = false; - if (cndFiles != null && cndFiles.size() > 0) - importNodeTypeDefinitions(repository); + // migrate data + Session session = null; + try { + session = login(); + for (JackrabbitDataModelMigration dataModelMigration : new TreeSet( + dataModelMigrations)) { + if (dataModelMigration.migrate(session)) { + restartAndClearCaches = true; + } + } + } catch (ArgeoException e) { + throw e; + } catch (Exception e) { + throw new ArgeoException("Cannot migrate", e); + } finally { + JcrUtils.logoutQuietly(session); + } - log.info("Initialized Jackrabbit repository " + repository + " in " - + homeDirectory + " with config " + configuration); + // restart repository + if (restartAndClearCaches) { + ((JackrabbitRepository) repository).shutdown(); + JackrabbitDataModelMigration.clearRepositoryCaches(getHome()); + repository = createJackrabbitRepository(); + } } - /** - * @deprecated explicitly declare {@link #dispose()} as destroy-method - * instead. - */ - public void destroy() throws Exception { - if (!alreadyDisposed) { - log.warn("## If not already done," - + " declare destroy-method=\"dispose\"."); - disposeImpl(); + /** Lazy init. */ + protected File getHome() { + if (home != null) + return home; + + try { + String osgiData = System.getProperty("osgi.instance.area"); + if (osgiData != null) + osgiData = osgiData.substring("file:".length()); + String path; + if (homeDirectory == null) + path = "./jackrabbit"; + else + path = homeDirectory; + if (path.startsWith(".") && osgiData != null) { + home = new File(osgiData + '/' + path).getCanonicalFile(); + } else + home = new File(path).getCanonicalFile(); + return home; + } catch (Exception e) { + throw new ArgeoException("Cannot define Jackrabbit home based on " + + homeDirectory, e); } } public void dispose() throws Exception { - disposeImpl(); - alreadyDisposed = true; - } - - protected void disposeImpl() throws Exception { if (repository != null) { if (repository instanceof JackrabbitRepository) ((JackrabbitRepository) repository).shutdown(); @@ -189,18 +234,30 @@ public class JackrabbitContainer implements InitializingBean, DisposableBean, } if (inMemory) - if (homeDirectory.exists()) { - FileUtils.deleteDirectory(homeDirectory); + if (getHome().exists()) { + FileUtils.deleteDirectory(getHome()); if (log.isDebugEnabled()) - log.debug("Deleted Jackrabbit home directory " - + homeDirectory); + log.debug("Deleted Jackrabbit home directory " + getHome()); } if (uri != null && !uri.trim().equals("")) log.info("Destroyed Jackrabbit repository with uri " + uri); else log.info("Destroyed Jackrabbit repository " + repository + " in " - + homeDirectory + " with config " + configuration); + + getHome() + " with config " + configuration); + } + + /** + * @deprecated explicitly declare {@link #dispose()} as destroy-method + * instead. + */ + public void destroy() throws Exception { + log.error("## Declare destroy-method=\"dispose\". in the Jackrabbit container bean"); + } + + /** @deprecated explicitly declare {@link #init()} as init-method instead. */ + public void afterPropertiesSet() throws Exception { + log.error("## Declare init-method=\"init\". in the Jackrabbit container bean"); } protected Properties getConfigurationProperties() { @@ -235,24 +292,12 @@ public class JackrabbitContainer implements InitializingBean, DisposableBean, * will be thrown. */ protected void importNodeTypeDefinitions(final Repository repository) { - final Credentials credentialsToUse = null; - // if (systemExecutor == null) { - // if (adminCredentials == null) { - // log.error("No system executor or admin credentials found," - // + " cannot import node types"); - // return; - // } - // credentialsToUse = adminCredentials; - // } else { - // credentialsToUse = null; - // } - Runnable action = new Runnable() { public void run() { Reader reader = null; Session session = null; try { - session = repository.login(credentialsToUse); + session = repository.login(); processNewSession(session); // Load cnds as resources for (String resUrl : cndFiles) { @@ -378,7 +423,7 @@ public class JackrabbitContainer implements InitializingBean, DisposableBean, } // BEANS METHODS - public void setHomeDirectory(File homeDirectory) { + public void setHomeDirectory(String homeDirectory) { this.homeDirectory = homeDirectory; } @@ -410,12 +455,13 @@ public class JackrabbitContainer implements InitializingBean, DisposableBean, this.systemExecutor = systemExecutor; } - public void setAdminCredentials(Credentials adminCredentials) { - this.adminCredentials = adminCredentials; - } - public void setRepository(Repository repository) { this.repository = repository; } + public void setDataModelMigrations( + Set dataModelMigrations) { + this.dataModelMigrations = dataModelMigrations; + } + } diff --git a/server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitDataModelMigration.java b/server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitDataModelMigration.java new file mode 100644 index 000000000..37d852907 --- /dev/null +++ b/server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitDataModelMigration.java @@ -0,0 +1,150 @@ +package org.argeo.jackrabbit; + +import java.io.File; +import java.io.InputStreamReader; +import java.io.Reader; + +import javax.jcr.Node; +import javax.jcr.Session; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.commons.cnd.CndImporter; +import org.argeo.ArgeoException; +import org.argeo.jcr.ArgeoNames; +import org.argeo.jcr.JcrCallback; +import org.argeo.jcr.JcrUtils; +import org.springframework.core.io.Resource; + +/** Migrate the data in a Jackrabbit repository. */ +public class JackrabbitDataModelMigration implements + Comparable { + private final static Log log = LogFactory + .getLog(JackrabbitDataModelMigration.class); + + private String dataModelNodePath; + private String targetVersion; + private Resource migrationCnd; + private JcrCallback dataModification; + + /** + * Expects an already started repository with the old data model to migrate. + * Expects to be run with admin rights (Repository.login() will be used). + * + * @return true if a migration was performed and the repository needs to be + * restarted and its caches cleared. + */ + public Boolean migrate(Session adminSession) { + long begin = System.currentTimeMillis(); + Reader reader = null; + try { + // check if already migrated + if (!adminSession.itemExists(dataModelNodePath)) { + log.warn("Node " + dataModelNodePath + + " does not exist: nothing to migrate."); + return false; + } + Node dataModelNode = adminSession.getNode(dataModelNodePath); + if (dataModelNode.hasProperty(ArgeoNames.ARGEO_DATA_MODEL_VERSION)) { + String currentVersion = dataModelNode.getProperty( + ArgeoNames.ARGEO_DATA_MODEL_VERSION).getString(); + if (compareVersions(currentVersion, targetVersion) >= 0) { + log.info("Data model at version " + currentVersion + + ", no need to migrate."); + return false; + } + } + + // apply transitional CND + reader = new InputStreamReader(migrationCnd.getInputStream()); + CndImporter.registerNodeTypes(reader, adminSession, true); + + // modify data + dataModification.execute(adminSession); + + // set data model version + dataModelNode.setProperty(ArgeoNames.ARGEO_DATA_MODEL_VERSION, + targetVersion); + + // apply changes + adminSession.save(); + + long duration = System.currentTimeMillis() - begin; + log.info("Migration of data model " + dataModelNodePath + " to " + + targetVersion + " performed in " + duration + "ms"); + return true; + } catch (Exception e) { + JcrUtils.discardQuietly(adminSession); + throw new ArgeoException("Migration of data model " + + dataModelNodePath + " to " + targetVersion + " failed.", + e); + } finally { + JcrUtils.logoutQuietly(adminSession); + IOUtils.closeQuietly(reader); + } + } + + protected static int compareVersions(String version1, String version2) { + // TODO do a proper version analysis and comparison + return version1.compareTo(version2); + } + + /** To be called on a stopped repository. */ + public static void clearRepositoryCaches(File home) { + File customNodeTypes = new File(home.getPath() + + "/repository/nodetypes/custom_nodetypes.xml"); + if (customNodeTypes.exists()) { + customNodeTypes.delete(); + if (log.isDebugEnabled()) + log.debug("Cleared " + customNodeTypes); + } else { + log.warn("File " + customNodeTypes + " not found."); + } + } + + /* + * FOR USE IN (SORTED) SETS + */ + + public int compareTo(JackrabbitDataModelMigration dataModelMigration) { + // TODO make ordering smarter + if (dataModelNodePath.equals(dataModelMigration.dataModelNodePath)) + return compareVersions(targetVersion, + dataModelMigration.targetVersion); + else + return dataModelNodePath + .compareTo(dataModelMigration.dataModelNodePath); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof JackrabbitDataModelMigration)) + return false; + JackrabbitDataModelMigration dataModelMigration = (JackrabbitDataModelMigration) obj; + return dataModelNodePath.equals(dataModelMigration.dataModelNodePath) + && targetVersion.equals(dataModelMigration.targetVersion); + } + + @Override + public int hashCode() { + return targetVersion.hashCode(); + } + + public void setDataModelNodePath(String dataModelNodePath) { + this.dataModelNodePath = dataModelNodePath; + } + + public void setTargetVersion(String targetVersion) { + this.targetVersion = targetVersion; + } + + public void setMigrationCnd(Resource migrationCnd) { + this.migrationCnd = migrationCnd; + } + + public void setDataModification(JcrCallback dataModification) { + this.dataModification = dataModification; + } + +} diff --git a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/ArgeoNames.java b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/ArgeoNames.java index 65f9e8d06..e24ca4386 100644 --- a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/ArgeoNames.java +++ b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/ArgeoNames.java @@ -6,6 +6,7 @@ public interface ArgeoNames { public final static String ARGEO_URI = "argeo:uri"; public final static String ARGEO_USER_ID = "argeo:userID"; + public final static String ARGEO_DATA_MODEL_VERSION = "argeo:dataModelVersion"; // user profile public final static String ARGEO_PROFILE = "argeo:profile"; diff --git a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrCallback.java b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrCallback.java new file mode 100644 index 000000000..5901497a3 --- /dev/null +++ b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrCallback.java @@ -0,0 +1,9 @@ +package org.argeo.jcr; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** An arbitrary execution on a JCR session, optionally returning a result. */ +public interface JcrCallback { + public Object execute(Session session); +} diff --git a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java index c7915d0ed..1cecd7100 100644 --- a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java +++ b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java @@ -167,7 +167,7 @@ public class JcrUtils implements ArgeoJcrConstants { buf.append('Y'); buf.append(cal.get(Calendar.YEAR)); buf.append('/'); - + int month = cal.get(Calendar.MONTH) + 1; buf.append('M'); if (month < 10) @@ -746,8 +746,13 @@ public class JcrUtils implements ArgeoJcrConstants { /** Logs out the session, not throwing any exception, even if it is null. */ public static void logoutQuietly(Session session) { - if (session != null) - session.logout(); + try { + if (session != null) + if (session.isLive()) + session.logout(); + } catch (Exception e) { + // silent + } } /** Returns the home node of the session user or null if none was found. */ -- 2.30.2