--- /dev/null
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jackrabbit;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.UUID;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.jackrabbit.api.JackrabbitRepository;
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.apache.jackrabbit.core.config.RepositoryConfig;
+import org.apache.jackrabbit.core.config.RepositoryConfigurationParser;
+import org.argeo.ArgeoException;
+import org.argeo.jcr.ArgeoNames;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.jcr.MaintainedRepository;
+import org.springframework.core.io.Resource;
+import org.springframework.util.SystemPropertyUtils;
+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 extends JackrabbitWrapper implements
+ MaintainedRepository {
+ private final static Log log = LogFactory.getLog(JackrabbitContainer.class);
+
+ // local
+ private Resource configuration;
+ private Resource variables;
+ private RepositoryConfig repositoryConfig;
+ private File homeDirectory;
+ private Boolean inMemory = false;
+
+ /** Migrations to execute (if not already done) */
+ private Set<JackrabbitDataModelMigration> dataModelMigrations = new HashSet<JackrabbitDataModelMigration>();
+
+ /**
+ * Empty constructor, {@link #init()} should be called after properties have
+ * been set
+ */
+ public JackrabbitContainer() {
+ }
+
+ public void init() {
+ long begin = System.currentTimeMillis();
+
+ if (getRepository() != null)
+ throw new ArgeoException(
+ "Cannot be used to wrap another repository");
+ Repository repository = createJackrabbitRepository();
+ super.setRepository(repository);
+
+ // migrate if needed
+ migrate();
+
+ // apply new CND files after migration
+ prepareDataModel();
+
+ double duration = ((double) (System.currentTimeMillis() - begin)) / 1000;
+ if (log.isDebugEnabled())
+ log.debug("Initialized JCR repository wrapper in " + duration
+ + " s");
+ }
+
+ /** Actually creates the new repository. */
+ protected Repository createJackrabbitRepository() {
+ long begin = System.currentTimeMillis();
+ InputStream configurationIn = null;
+ Repository repository;
+ try {
+ // temporary
+ if (inMemory && getHomeDirectory().exists()) {
+ FileUtils.deleteDirectory(getHomeDirectory());
+ log.warn("Deleted Jackrabbit home directory "
+ + getHomeDirectory());
+ }
+
+ // process configuration file
+ Properties vars = getConfigurationProperties();
+ configurationIn = readConfiguration();
+ vars.put(RepositoryConfigurationParser.REPOSITORY_HOME_VARIABLE,
+ getHomeDirectory().getCanonicalPath());
+ repositoryConfig = RepositoryConfig.create(new InputSource(
+ configurationIn), vars);
+
+ //
+ // Actual repository creation
+ //
+ repository = RepositoryImpl.create(repositoryConfig);
+
+ double duration = ((double) (System.currentTimeMillis() - begin)) / 1000;
+ if (log.isTraceEnabled())
+ log.trace("Created Jackrabbit repository in " + duration
+ + " s, home: " + getHomeDirectory());
+
+ return repository;
+ } catch (Exception e) {
+ throw new ArgeoException("Cannot create Jackrabbit repository "
+ + getHomeDirectory(), e);
+ } finally {
+ IOUtils.closeQuietly(configurationIn);
+ }
+ }
+
+ /** Lazy init. */
+ protected File getHomeDirectory() {
+ try {
+ if (homeDirectory == null) {
+ if (inMemory) {
+ homeDirectory = new File(
+ System.getProperty("java.io.tmpdir")
+ + File.separator
+ + System.getProperty("user.name")
+ + File.separator + "jackrabbit-"
+ + UUID.randomUUID());
+ homeDirectory.mkdirs();
+ // will it work if directory is not empty??
+ homeDirectory.deleteOnExit();
+ }
+ }
+
+ return homeDirectory.getCanonicalFile();
+ } catch (IOException e) {
+ throw new ArgeoException("Cannot get canonical file for "
+ + homeDirectory, e);
+ }
+ }
+
+ /** Executes migrations, if needed. */
+ protected void migrate() {
+ // No migration to perform
+ if (dataModelMigrations.size() == 0)
+ return;
+
+ Boolean restartAndClearCaches = false;
+
+ // 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);
+ }
+
+ // restart repository
+ if (restartAndClearCaches) {
+ Repository repository = getRepository();
+ if (repository instanceof RepositoryImpl) {
+ JackrabbitDataModelMigration
+ .clearRepositoryCaches(((RepositoryImpl) repository)
+ .getConfig());
+ }
+ ((JackrabbitRepository) repository).shutdown();
+ createJackrabbitRepository();
+ }
+
+ // set data model version
+ try {
+ session = login();
+ } catch (RepositoryException e) {
+ throw new ArgeoException("Cannot login to migrated repository", e);
+ }
+
+ for (JackrabbitDataModelMigration dataModelMigration : new TreeSet<JackrabbitDataModelMigration>(
+ dataModelMigrations)) {
+ try {
+ if (session.itemExists(dataModelMigration
+ .getDataModelNodePath())) {
+ Node dataModelNode = session.getNode(dataModelMigration
+ .getDataModelNodePath());
+ dataModelNode.setProperty(
+ ArgeoNames.ARGEO_DATA_MODEL_VERSION,
+ dataModelMigration.getTargetVersion());
+ session.save();
+ }
+ } catch (Exception e) {
+ log.error("Cannot set model version", e);
+ }
+ }
+ JcrUtils.logoutQuietly(session);
+
+ }
+
+ /** Shutdown the repository */
+ public void destroy() throws Exception {
+ Repository repository = getRepository();
+ if (repository != null && repository instanceof RepositoryImpl) {
+ long begin = System.currentTimeMillis();
+ ((RepositoryImpl) repository).shutdown();
+ if (inMemory)
+ if (getHomeDirectory().exists()) {
+ FileUtils.deleteDirectory(getHomeDirectory());
+ if (log.isDebugEnabled())
+ log.debug("Deleted Jackrabbit home directory "
+ + getHomeDirectory());
+ }
+ double duration = ((double) (System.currentTimeMillis() - begin)) / 1000;
+ log.info("Destroyed Jackrabbit repository in " + duration
+ + " s, home: " + getHomeDirectory());
+ }
+ repository = null;
+ }
+
+ public void dispose() {
+ throw new IllegalArgumentException(
+ "Call destroy() method instead of dispose()");
+ }
+
+ /*
+ * UTILITIES
+ */
+ /**
+ * Reads the configuration which will initialize a {@link RepositoryConfig}.
+ */
+ protected InputStream readConfiguration() {
+ try {
+ return configuration != null ? configuration.getInputStream()
+ : null;
+ } catch (IOException e) {
+ throw new ArgeoException("Cannot read Jackrabbit configuration "
+ + configuration, e);
+ }
+ }
+
+ /**
+ * Reads the variables which will initialize a {@link Properties}. Returns
+ * null by default, to be overridden.
+ *
+ * @return a new stream or null if no variables available
+ */
+ protected InputStream readVariables() {
+ try {
+ return variables != null ? variables.getInputStream() : null;
+ } catch (IOException e) {
+ throw new ArgeoException("Cannot read Jackrabbit variables "
+ + variables, e);
+ }
+ }
+
+ /**
+ * Resolves ${} placeholders in the provided string. Based on system
+ * properties if no map is provided.
+ */
+ protected String resolvePlaceholders(String string,
+ Map<String, String> variables) {
+ return SystemPropertyUtils.resolvePlaceholders(string);
+ }
+
+ /** Generates the properties to use in the configuration. */
+ protected Properties getConfigurationProperties() {
+ InputStream propsIn = null;
+ Properties vars;
+ try {
+ vars = new Properties();
+ propsIn = readVariables();
+ if (propsIn != null) {
+ vars.load(propsIn);
+ }
+ // resolve system properties
+ for (Object key : vars.keySet()) {
+ // TODO: implement a smarter mechanism to resolve nested ${}
+ String newValue = resolvePlaceholders(
+ vars.getProperty(key.toString()), null);
+ vars.put(key, newValue);
+ }
+ // override with system properties
+ vars.putAll(System.getProperties());
+
+ if (log.isTraceEnabled()) {
+ log.trace("Jackrabbit config variables:");
+ for (Object key : new TreeSet<Object>(vars.keySet()))
+ log.trace(key + "=" + vars.getProperty(key.toString()));
+ }
+
+ } catch (IOException e) {
+ throw new ArgeoException("Cannot read configuration properties", e);
+ } finally {
+ IOUtils.closeQuietly(propsIn);
+ }
+ return vars;
+ }
+
+ /*
+ * FIELDS ACCESS
+ */
+
+ public void setHomeDirectory(File homeDirectory) {
+ this.homeDirectory = homeDirectory;
+ }
+
+ public void setInMemory(Boolean inMemory) {
+ this.inMemory = inMemory;
+ }
+
+ public void setRepository(Repository repository) {
+ throw new ArgeoException("Cannot be used to wrap another repository");
+ }
+
+ public void setDataModelMigrations(
+ Set<JackrabbitDataModelMigration> dataModelMigrations) {
+ this.dataModelMigrations = dataModelMigrations;
+ }
+
+ public void setVariables(Resource variables) {
+ this.variables = variables;
+ }
+
+ public void setConfiguration(Resource configuration) {
+ this.configuration = configuration;
+ }
+
+}