org.modeshape.jcr.ModeShapeEngine Maven / Gradle / Ivy
/*
* 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.Callable;
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.infinispan.schematic.document.Changes;
import org.infinispan.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 repositories = new HashMap();
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.
*
* This is equivalent to calling shutdown(true)
*
* @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 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 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(new Callable() {
@Override
public Boolean call() throws Exception {
return 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> repoFutures = new LinkedList>();
Queue repoNames = new LinkedList();
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 e) {
Logger.getLogger(getClass()).error(e, JcrI18n.failedToShutdownDeployedRepository, repoName);
} catch (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 getRepositoryNames() {
checkRunning();
final Lock lock = this.lock.readLock();
try {
lock.lock();
Set names = new HashSet();
for (JcrRepository repository : repositories.values()) {
names.add(repository.getName());
}
return names;
} finally {
lock.unlock();
}
}
protected Set getRepositoryKeys() {
checkRunning();
final Lock lock = this.lock.readLock();
try {
lock.lock();
return new HashSet(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.
*
* 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 get
methods.
*
*
* @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 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(new Callable() {
@Override
public JcrRepository call() throws Exception {
// 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.
*
* 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 get
methods.
*
*
* @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 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 getRepositories() {
checkRunning();
Map results = new HashMap();
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 repositories() {
if (this.state == State.RUNNING) {
final Lock lock = this.lock.readLock();
try {
lock.lock();
return new ArrayList(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.
*
* The basic outline for modifying a repository's configuration is as follows:
*
* - Get the current configuration for the repository that is to be changed
* - Get an editor from that configuration
* - Use the editor to capture and validate the changes you want to make
* - Update the repository's configuration with the changes from the editor
*
* Here's some code that shows how this is done:
*
*
* 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();
* }
*
*
*
*
* 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.
*
*
* @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 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(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 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();
}
}
}