/* * ModeShape (http://www.modeshape.org) * * 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.modeshape.jcr; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.jcr.Repository; import javax.jcr.RepositoryException; import org.modeshape.schematic.document.Changes; import org.modeshape.schematic.document.Editor; import org.modeshape.common.annotation.ThreadSafe; import org.modeshape.common.collection.Problems; import org.modeshape.common.logging.Logger; import org.modeshape.common.util.CheckArg; import org.modeshape.common.util.ImmediateFuture; import org.modeshape.common.util.NamedThreadFactory; import org.modeshape.jcr.api.Repositories; /** * A container for repositories. */ @ThreadSafe public class ModeShapeEngine implements Repositories { private final ReadWriteLock lock = new ReentrantReadWriteLock(); private static final Logger LOGGER = Logger.getLogger(ModeShapeEngine.class); private final Map<String, JcrRepository> repositories = new HashMap<String, JcrRepository>(); private ExecutorService repositoryStarterService; private volatile State state = State.NOT_RUNNING; public enum State { NOT_RUNNING, STARTING, RUNNING, RESTORING, STOPPING } public ModeShapeEngine() { } protected final boolean checkRunning() { if (state == State.RUNNING) return true; throw new IllegalStateException(JcrI18n.engineIsNotRunning.text()); } /** * Get the running state of this engine. * * @return the current state; never null */ public State getState() { return state; } /** * Start this engine to make it available for use. This method does nothing if the engine is already running. * * @see #shutdown() */ public void start() { if (state == State.RUNNING) return; final Lock lock = this.lock.writeLock(); try { lock.lock(); this.state = State.STARTING; // Create an executor service that we'll use to start the repositories ... ThreadFactory threadFactory = new NamedThreadFactory("modeshape-start-repo"); repositoryStarterService = Executors.newCachedThreadPool(threadFactory); state = State.RUNNING; } catch (RuntimeException e) { state = State.NOT_RUNNING; throw e; } finally { lock.unlock(); } } /** * Shutdown this engine to stop all repositories, terminate any ongoing background operations (such as sequencing), and * reclaim any resources that were acquired by this engine. This method may be called multiple times, but only the first time * has an effect. * <p> * This is equivalent to calling <code>shutdown(true)</code> * * @return a future that allows the caller to block until the engine is shutdown; any error during shutdown will be thrown * when {@link Future#get() getting} the result from the future, where the exception is wrapped in a * {@link ExecutionException}. The value returned from the future will always be true if the engine shutdown (or was * not running), or false if the engine is still running. * @see #start() */ public Future<Boolean> shutdown() { return shutdown(true); } /** * Shutdown this engine, optionally stopping all still-running repositories. * * @param forceShutdownOfAllRepositories true if the engine should be shutdown even if there are currently-running * repositories, or false if the engine should not be shutdown if at least one repository is still running. * @return a future that allows the caller to block until the engine is shutdown; any error during shutdown will be thrown * when {@link Future#get() getting} the repository from the future, where the exception is wrapped in a * {@link ExecutionException}. The value returned from the future will always be true if the engine shutdown (or was * not running), or false if the engine is still running. * @see #start() */ public Future<Boolean> shutdown( boolean forceShutdownOfAllRepositories ) { if (!forceShutdownOfAllRepositories) { // Check to see if there are any still running ... final Lock lock = this.lock.readLock(); try { lock.lock(); for (JcrRepository repository : repositories.values()) { switch (repository.getState()) { case NOT_RUNNING: case STOPPING: break; case RESTORING: case RUNNING: case STARTING: // This repository is still running, so fail return ImmediateFuture.create(Boolean.FALSE); } } // If we got to here, there are no more running repositories ... } finally { lock.unlock(); } } // Create a simple executor that will do the backgrounding for us ... final ExecutorService executor = Executors.newSingleThreadExecutor(); try { // Submit a runnable to shutdown the repositories ... return executor.submit(this::doShutdown); } finally { // Now shutdown the executor and return the future ... executor.shutdown(); } } /** * Do the work of shutting down this engine and its repositories. * * @return true if the engine was shutdown (or was not running), and false if the engine is still running and could not be * shutdown */ protected boolean doShutdown() { if (state == State.NOT_RUNNING) { LOGGER.debug("Engine already shut down."); return true; } LOGGER.debug("Shutting down engine..."); final Lock lock = this.lock.writeLock(); try { lock.lock(); state = State.STOPPING; if (!repositories.isEmpty()) { // Now go through all of the repositories and request they all be shutdown ... Queue<Future<Boolean>> repoFutures = new LinkedList<Future<Boolean>>(); Queue<String> repoNames = new LinkedList<String>(); for (JcrRepository repository : repositories.values()) { if (repository != null) { repoNames.add(repository.getName()); repoFutures.add(repository.shutdown()); } } // Now block while each is shutdown ... while (repoFutures.peek() != null) { String repoName = repoNames.poll(); try { // Get the results from the future (this will return only when the shutdown has completed) ... repoFutures.poll().get(); // We've successfully shut down, so remove it from the map ... repositories.remove(repoName); } catch (ExecutionException | InterruptedException e) { Logger.getLogger(getClass()).error(e, JcrI18n.failedToShutdownDeployedRepository, repoName); } } } if (repositories.isEmpty()) { // All repositories were properly shutdown, so now stop the service for starting and shutting down the repos ... repositoryStarterService.shutdown(); repositoryStarterService = null; // Do not clear the set of repositories, so that restarting will work just fine ... this.state = State.NOT_RUNNING; } else { // Could not shut down all repositories, so keep running .. this.state = State.RUNNING; } } catch (RuntimeException e) { this.state = State.RUNNING; throw e; } finally { lock.unlock(); } return this.state != State.RUNNING; } /** * Get the deployed {@link Repository} instance with the given the name. * * @param repositoryName the name of the deployed repository * @return the named repository instance * @throws IllegalArgumentException if the repository name is null, blank or invalid * @throws NoSuchRepositoryException if there is no repository with the specified name * @throws IllegalStateException if this engine is not {@link #getState() running} * @see #deploy(RepositoryConfiguration) * @see #undeploy(String) */ @Override public final JcrRepository getRepository( String repositoryName ) throws NoSuchRepositoryException { CheckArg.isNotEmpty(repositoryName, "repositoryName"); checkRunning(); final Lock lock = this.lock.readLock(); try { lock.lock(); JcrRepository repository = repositories.get(repositoryName); if (repository == null) { throw new NoSuchRepositoryException(JcrI18n.repositoryDoesNotExist.text(repositoryName)); } return repository; } finally { lock.unlock(); } } @Override public Set<String> getRepositoryNames() { checkRunning(); final Lock lock = this.lock.readLock(); try { lock.lock(); Set<String> names = new HashSet<String>(); for (JcrRepository repository : repositories.values()) { names.add(repository.getName()); } return names; } finally { lock.unlock(); } } protected Set<String> getRepositoryKeys() { checkRunning(); final Lock lock = this.lock.readLock(); try { lock.lock(); return new HashSet<String>(repositories.keySet()); } finally { lock.unlock(); } } /** * Get the state of the deployed {@link Repository} instance with the given the name. * * @param repositoryName the name of the deployed repository * @return the state of the repository instance; never null * @throws IllegalArgumentException if the repository name is null, blank or invalid * @throws NoSuchRepositoryException if there is no repository with the specified name * @throws IllegalStateException if this engine is not {@link #getState() running} * @see #deploy(RepositoryConfiguration) * @see #undeploy(String) */ public final State getRepositoryState( String repositoryName ) throws NoSuchRepositoryException { return getRepository(repositoryName).getState(); } /** * Get the immutable configuration for the repository with the supplied name. * * @param repositoryName the name of the deployed repository * @return the repository configuration; never null * @throws IllegalArgumentException if the repository name is null, blank or invalid * @throws NoSuchRepositoryException if there is no repository with the specified name * @throws IllegalStateException if this engine is not {@link #getState() running} */ public final RepositoryConfiguration getRepositoryConfiguration( String repositoryName ) throws NoSuchRepositoryException { return getRepository(repositoryName).getConfiguration(); } /** * Asynchronously start the deployed {@link Repository} instance with the given the name, and return a future that will return * the Repository instance. If the Repository is already running, this method returns a future that returns immediately. * <p> * Note that the caller does not have to wait for the startup process to complete. However, to do so the caller merely calls * {@link Future#get() get()} or {@link Future#get(long, TimeUnit) get(long,TimeUnit)} on the future to return the repository * instance. Note that any exceptions thrown during the startup process will be wrapped in an {@link ExecutionException} * thrown by the Future's <code>get</code> methods. * </p> * * @param repositoryName the name of the deployed repository * @return the state of the repository instance; never null * @throws IllegalArgumentException if the repository name is null, blank or invalid * @throws NoSuchRepositoryException if there is no repository with the specified name * @throws IllegalStateException if this engine is not {@link #getState() running} * @see #deploy(RepositoryConfiguration) * @see #undeploy(String) */ public final Future<JcrRepository> startRepository( final String repositoryName ) throws NoSuchRepositoryException { final JcrRepository repository = getRepository(repositoryName); if (repository.getState() == State.RUNNING) { return ImmediateFuture.create(repository); } // Create an initializer that will start the repository ... return repositoryStarterService.submit(() -> { // Instantiate (and start) the repository ... try { repository.start(); return repository; } catch (Exception e) { // Something went wrong, so undeploy the repository ... undeploy(repositoryName); throw e; } }); } /** * Asynchronously shutdown the deployed {@link Repository} instance with the given the name, and return a future that will * return whether the Repository instance is shutdown. If the Repository is not running, the resulting future will return * immediately. * <p> * Note that the caller does not have to wait for the shutdown to completed. However, to do so the caller merely calls * {@link Future#get() get()} or {@link Future#get(long, TimeUnit) get(long,TimeUnit)} on the future to return a boolean flag * specifying whether the Repository instance is shutdown (not running). Note that any exceptions thrown during the shutdown * will be wrapped in an {@link ExecutionException} thrown by the Future's <code>get</code> methods. * </p> * * @param repositoryName the name of the deployed repository * @return a future wrapping the asynchronous shutdown process; never null, and {@link Future#get()} will return whether the * @throws IllegalArgumentException if the repository name is null, blank or invalid * @throws NoSuchRepositoryException if there is no repository with the specified name * @throws IllegalStateException if this engine is not {@link #getState() running} * @see #deploy(RepositoryConfiguration) * @see #undeploy(String) */ public final Future<Boolean> shutdownRepository( String repositoryName ) throws NoSuchRepositoryException { return getRepository(repositoryName).shutdown(); } /** * Get an instantaneous snapshot of the JCR repositories and their state. Note that the results are accurate only when this * methods returns. * * @return the immutable map of repository states keyed by repository names; never null */ public Map<String, State> getRepositories() { checkRunning(); Map<String, State> results = new HashMap<String, State>(); final Lock lock = this.lock.readLock(); try { lock.lock(); for (JcrRepository repository : repositories.values()) { results.put(repository.getName(), repository.getState()); } } finally { lock.unlock(); } return Collections.unmodifiableMap(results); } /** * Returns a copy of the repositories. Note that when returned, not all repositories may be active. * * @return a copy of the repositories; never null */ protected Collection<JcrRepository> repositories() { if (this.state == State.RUNNING) { final Lock lock = this.lock.readLock(); try { lock.lock(); return new ArrayList<JcrRepository>(repositories.values()); } finally { lock.unlock(); } } return Collections.emptyList(); } /** * Deploy a new repository with the given configuration. This method will fail if this engine already contains a repository * with the specified name. * * @param repositoryConfiguration the configuration for the repository * @return the deployed repository instance, which must be {@link #startRepository(String) started} before it can be used; * never null * @throws ConfigurationException if the configuration is not valid * @throws RepositoryException if there is already a deployed repository with the specified name, or if there is a problem * deploying the repository * @throws IllegalArgumentException if the configuration is null * @see #deploy(RepositoryConfiguration) * @see #update(String, Changes) * @see #undeploy(String) */ public JcrRepository deploy( final RepositoryConfiguration repositoryConfiguration ) throws ConfigurationException, RepositoryException { return deploy(repositoryConfiguration, null); } /** * Deploy a new repository with the given configuration. This method will fail if this engine already contains a repository * with the specified name. * * @param repositoryConfiguration the configuration for the repository * @param repositoryKey the key by which this repository is known to this engine; may be null if the * {@link RepositoryConfiguration#getName() repository's name} should be used * @return the deployed repository instance, which must be {@link #startRepository(String) started} before it can be used; * never null * @throws ConfigurationException if the configuration is not valid * @throws RepositoryException if there is already a deployed repository with the specified name, or if there is a problem * deploying the repository * @throws IllegalArgumentException if the configuration is null * @see #deploy(RepositoryConfiguration) * @see #update(String, Changes) * @see #undeploy(String) */ protected JcrRepository deploy( final RepositoryConfiguration repositoryConfiguration, final String repositoryKey ) throws ConfigurationException, RepositoryException { CheckArg.isNotNull(repositoryConfiguration, "repositoryConfiguration"); checkRunning(); final String repoName = repositoryKey != null ? repositoryKey : repositoryConfiguration.getName(); Problems problems = repositoryConfiguration.validate(); if (problems.hasErrors()) { throw new ConfigurationException(problems, JcrI18n.repositoryConfigurationIsNotValid.text(repoName, problems.toString())); } // Now try to deploy the repository ... JcrRepository repository = null; final Lock lock = this.lock.writeLock(); try { lock.lock(); if (this.repositories.containsKey(repoName)) { throw new RepositoryException(JcrI18n.repositoryIsAlreadyDeployed.text(repoName)); } // Instantiate (but do not start!) the repository, store it in our map, and return it ... repository = new JcrRepository(repositoryConfiguration); this.repositories.put(repoName, repository); } finally { lock.unlock(); } return repository; } /** * Update the configuration of a deployed repository by applying the set of changes to that repository's configuration. The * changes can be built by obtaining the configuration for the deployed instance, obtaining an editor (which actually contains * a copy of the configuration) and using it to make changes, and then getting the changes from the editor. The benefit of * this approach is that the changes can be made in an isolated copy of the configuration and all of the changes applied en * masse. * <p> * The basic outline for modifying a repository's configuration is as follows: * <ol> * <li>Get the current configuration for the repository that is to be changed</li> * <li>Get an editor from that configuration</li> * <li>Use the editor to capture and validate the changes you want to make</li> * <li>Update the repository's configuration with the changes from the editor</li> * </ol> * Here's some code that shows how this is done: * * <pre> * ModeShapeEngine engine = ... * Repository deployed = engine.{@link ModeShapeEngine#getRepository(String) getRepository("repo")}; * RepositoryConfiguration deployedConfig = deployed.{@link JcrRepository#getConfiguration() getConfiguration()}; * * // Create an editor, which is actually manipulating a copy of the configuration document ... * Editor editor = deployedConfig.{@link RepositoryConfiguration#edit() edit()}; * * // Modify the copy of the configuration (we'll do something trivial here) ... * editor.setNumber(FieldName.LARGE_VALUE_SIZE_IN_BYTES,8096); * * // Get our changes and validate them ... * Changes changes = editor.{@link Editor#getChanges() getChanges()}; * Results validationResults = deployedConfig.{@link RepositoryConfiguration#validate(Changes) validate(changes)}; * if ( validationResults.hasErrors() ) { * // you've done something wrong with your editor * } else { * // Update the deployed repository's configuration with these changes ... * Future<Boolean> future = engine.{@link ModeShapeEngine#update(String, Changes) update("repo",changes)}; * * // Optionally block while the repository instance is changed to * // reflect the new configuration ... * JcrRepository updated = future.get(); * } * </pre> * * </p> * <p> * Note that this method blocks while the changes to the configuration are validated and applied, but before the repository * has changed to reflect the new configuration, which is done asynchronously. The resulting future represents that * asynchronous process, and the future can be used to block until that updating is completed. * </p> * * @param repositoryName the name of the repository * @param changes the changes that should be applied to the repository's configuration * @return a future that allows the caller to block until the repository has completed all of its changes. * @throws ConfigurationException if the configuration is not valid with the supplied changes * @throws NoSuchRepositoryException if there is no repository with the specified name * @throws RepositoryException if there is a problem updating the repository * @throws IllegalArgumentException if any of the parameters are null or invalid * @see #deploy(RepositoryConfiguration) * @see #undeploy(String) */ public Future<JcrRepository> update( final String repositoryName, final Changes changes ) throws ConfigurationException, NoSuchRepositoryException, RepositoryException { final Lock lock = this.lock.writeLock(); try { lock.lock(); // Get the repository ... final JcrRepository repository = this.repositories.get(repositoryName); if (repository == null) { // There is no repository with this name ... throw new NoSuchRepositoryException(JcrI18n.repositoryDoesNotExist.text(repositoryName)); } // Determine if the changes would result in a valid repository configuration ... RepositoryConfiguration config = repository.getConfiguration(); Problems problems = config.validate(changes); repository.setConfigurationProblems(problems); if (problems.hasErrors()) { throw new ConfigurationException(problems, JcrI18n.repositoryConfigurationIsNotValid.text(repositoryName, problems.toString())); } // Apply the changes immediately (synchronously) ... try { repository.apply(changes); } catch (Exception e) { throw new ConfigurationException(problems, JcrI18n.repositoryConfigurationIsNotValid.text(repositoryName, e.getMessage()), e); } return new ImmediateFuture<JcrRepository>(repository); } finally { lock.unlock(); } } /** * Stop and undeploy the named {@link Repository}. * * @param repositoryName the name of the deployed repository * @return a future that allows the caller to block until the repository is shutdown; if the repository could not shutdown, * {@link Future#get() getting} the repository from the future will throw the exception wrapped in a * {@link ExecutionException} * @throws IllegalArgumentException if the repository name is null, blank or invalid * @throws NoSuchRepositoryException if there is no repository with the specified name * @throws IllegalStateException if this engine was not {@link #start() started} */ public Future<Boolean> undeploy( final String repositoryName ) throws NoSuchRepositoryException { CheckArg.isNotEmpty(repositoryName, "repositoryName"); checkRunning(); // Now try to undeploy the repository ... final Lock lock = this.lock.writeLock(); try { lock.lock(); final JcrRepository repository = this.repositories.remove(repositoryName); if (repository == null) { // There is no repository with this name ... throw new NoSuchRepositoryException(JcrI18n.repositoryDoesNotExist.text(repositoryName)); } // There is an existing repository, so start to shut it down (note that it may fail) ... return repository.shutdown(); } finally { lock.unlock(); } } }