Introduce Jackrabbit data model migration
authorMathieu Baudier <mbaudier@argeo.org>
Mon, 6 Jun 2011 15:55:32 +0000 (15:55 +0000)
committerMathieu Baudier <mbaudier@argeo.org>
Mon, 6 Jun 2011 15:55:32 +0000 (15:55 +0000)
git-svn-id: https://svn.argeo.org/commons/trunk@4560 4cfe0d0a-d680-48aa-b62c-e0a02a3f76cc

server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitAuthorizations.java
server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitContainer.java
server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitDataModelMigration.java [new file with mode: 0644]
server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/ArgeoNames.java
server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrCallback.java [new file with mode: 0644]
server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java

index ee0312164c56ddef857564aabc54dca5e8408de8..8c1204c0e09b57b2296015cadd00520bf0ea7be2 100644 (file)
@@ -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<String, String> groupPrivileges = new HashMap<String, String>();
 
        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)
index 294fdd5a3ec6d01f667bb1e360f73a7d868e6ca0..e27a473b5e041cf6bd2baa99ec2df943707e2973 100644 (file)
@@ -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<String> cndFiles = new ArrayList<String>();
 
+       /** Migrations to execute (if not already done) */
+       private Set<JackrabbitDataModelMigration> dataModelMigrations = new HashSet<JackrabbitDataModelMigration>();
+
        /** Namespaces to register: key is prefix, value namespace */
        private Map<String, String> namespaces = new HashMap<String, String>();
 
        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<String, String> params = new HashMap<String, String>();
-                       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<String, String> params = new HashMap<String, String>();
+                               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<JackrabbitDataModelMigration>(
+                                       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<JackrabbitDataModelMigration> 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 (file)
index 0000000..37d8529
--- /dev/null
@@ -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<JackrabbitDataModelMigration> {
+       private final static Log log = LogFactory
+                       .getLog(JackrabbitDataModelMigration.class);
+
+       private String dataModelNodePath;
+       private String targetVersion;
+       private Resource migrationCnd;
+       private JcrCallback dataModification;
+
+       /**
+        * Expects an already started repository with the old data model to migrate.
+        * Expects to be run with admin rights (Repository.login() will be used).
+        * 
+        * @return true if a migration was performed and the repository needs to be
+        *         restarted and its caches cleared.
+        */
+       public Boolean migrate(Session 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;
+       }
+
+}
index 65f9e8d06dc942361f98afba756df5bdbd9b2329..e24ca43865844db66ed2c38dd4b89f90dcfbff0c 100644 (file)
@@ -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 (file)
index 0000000..5901497
--- /dev/null
@@ -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);
+}
index c7915d0ed57bb89ed132419247c3eebc18037ff7..1cecd7100611744524da8e08296dbc49f00b935a 100644 (file)
@@ -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. */