/*
* 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.jboss.service;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.jcr.RepositoryException;
import org.jboss.as.controller.OperationFailedException;
import org.jboss.dmr.ModelNode;
import org.jboss.logging.Logger;
import org.jboss.modules.ModuleIdentifier;
import org.jboss.modules.ModuleLoadException;
import org.jboss.modules.ModuleLoader;
import org.jboss.msc.service.Service;
import org.jboss.msc.service.StartContext;
import org.jboss.msc.service.StartException;
import org.jboss.msc.service.StopContext;
import org.jboss.msc.value.InjectedValue;
import org.jboss.security.ISecurityManagement;
import org.jgroups.Channel;
import org.jgroups.JChannel;
import org.jgroups.conf.XmlConfigurator;
import org.modeshape.common.collection.Problems;
import org.modeshape.common.util.DelegatingClassLoader;
import org.modeshape.common.util.StringUtil;
import org.modeshape.jboss.subsystem.MappedAttributeDefinition;
import org.modeshape.jcr.Environment;
import org.modeshape.jcr.JcrRepository;
import org.modeshape.jcr.ModeShapeEngine;
import org.modeshape.jcr.NoSuchRepositoryException;
import org.modeshape.jcr.RepositoryConfiguration;
import org.modeshape.jcr.RepositoryConfiguration.FieldName;
import org.modeshape.jcr.RepositoryStatistics;
import org.modeshape.schematic.Schematic;
import org.modeshape.schematic.document.Changes;
import org.modeshape.schematic.document.EditableArray;
import org.modeshape.schematic.document.EditableDocument;
import org.modeshape.schematic.document.Editor;
import org.wildfly.clustering.jgroups.ChannelFactory;
/**
* A <code>RepositoryService</code> instance is the service responsible for initializing a {@link JcrRepository} in the ModeShape
* engine using the information from the configuration.
*/
public class RepositoryService implements Service<JcrRepository>, Environment {
private static final Logger LOG = Logger.getLogger(RepositoryService.class.getPackage().getName());
private final InjectedValue<ModeShapeEngine> engineInjector = new InjectedValue<>();
private final InjectedValue<BinaryStorage> binaryStorageInjector = new InjectedValue<>();
private final InjectedValue<String> dataDirectoryPathInjector = new InjectedValue<>();
private final InjectedValue<ModuleLoader> moduleLoaderInjector = new InjectedValue<>();
private final InjectedValue<RepositoryStatistics> monitorInjector = new InjectedValue<>();
private final InjectedValue<ChannelFactory> channelFactoryInjector = new InjectedValue<>();
private final InjectedValue<ISecurityManagement> securityManagementServiceInjector = new InjectedValue<>();
private final RepositoryConfiguration repositoryConfiguration;
private final Set<ModuleIdentifier> additionalModuleDependencies = new LinkedHashSet<>();
private String journalPath;
private String journalRelativeTo;
public RepositoryService(RepositoryConfiguration repositoryConfiguration, String additionalModuleDependencies) {
this.repositoryConfiguration = repositoryConfiguration;
if (!StringUtil.isBlank(additionalModuleDependencies)) {
for (String moduleName : additionalModuleDependencies.split(",")) {
ModuleIdentifier moduleId = moduleIdentifierFromName(moduleName);
if (moduleId != null) {
this.additionalModuleDependencies.add(moduleId);
}
}
}
}
@Override
public JcrRepository getValue() throws IllegalStateException, IllegalArgumentException {
try {
return getEngine().getRepository(repositoryName());
} catch (NoSuchRepositoryException e) {
throw new IllegalStateException(e);
}
}
private ModeShapeEngine getEngine() {
return engineInjector.getValue();
}
@Override
public ClassLoader getClassLoader( Object caller,
String... classpathEntries ) {
caller = Objects.requireNonNull(caller, "caller");
Stream<ModuleIdentifier> optionalModuleIds = Arrays.stream(classpathEntries)
.filter(Objects::nonNull)
.map(this::moduleIdentifierFromName)
.filter(Objects::nonNull);
List<ClassLoader> delegatingLoaders = Stream.concat(optionalModuleIds, this.additionalModuleDependencies.stream())
.map(moduleId -> {
try {
return moduleLoader().loadModule(moduleId).getClassLoader();
} catch (ModuleLoadException e) {
LOG.warnv(e,
"Cannot load module from classpath entry with identifier: {0}",
moduleId);
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
ClassLoader currentLoader = getClass().getClassLoader();
ClassLoader callerLoader = caller.getClass().getClassLoader();
if (!callerLoader.equals(currentLoader)) {
// if the parent of fallback is the same as the current loader, just use that
if (callerLoader.getParent().equals(currentLoader)) {
currentLoader = callerLoader;
} else {
delegatingLoaders.add(callerLoader);
}
}
return delegatingLoaders.isEmpty() ? currentLoader : new DelegatingClassLoader(currentLoader, delegatingLoaders);
}
private ModuleIdentifier moduleIdentifierFromName(String moduleName) {
if (StringUtil.isBlank(moduleName)) {
return null;
}
try {
return ModuleIdentifier.fromString(moduleName.trim());
} catch (IllegalArgumentException e) {
LOG.warnv("{0} is not a valid module identifier", moduleName);
return null;
}
}
@Override
public void shutdown() {
// Do nothing; this is the Environment's shutdown method
}
@Override
public Channel getChannel(String name) throws Exception {
LOG.debugv("getting JGroups channel named '{0}'", name);
final ChannelFactory channelFactory = channelFactoryInjector.getOptionalValue();
if (channelFactory != null) {
LOG.debugv("JGroups configured to use server subsystem stack");
// there is a cluster-stack attribute configured, so use that
return channelFactory.createChannel(name);
}
// there is no cluster stack, so use a configured XML file
String clusterConfig = repositoryConfiguration.getClustering().getConfiguration();
assert clusterConfig != null;
LOG.debugv("reading JGroups config '{0}'", clusterConfig);
InputStream configStream = new FileInputStream(clusterConfig);
XmlConfigurator configurator = XmlConfigurator.getInstance(configStream);
return new JChannel(configurator);
}
public final String repositoryName() {
return repositoryConfiguration.getName();
}
@Override
public void start( StartContext arg0 ) throws StartException {
ModeShapeEngine engine = getEngine();
try {
final String repositoryName = repositoryName();
// Get the binary storage configuration ...
BinaryStorage binaryStorageConfig = binaryStorageInjector.getValue();
assert binaryStorageConfig != null;
EditableDocument binaryConfig = binaryStorageConfig.getBinaryConfiguration();
// Create a new configuration document ...
EditableDocument config = Schematic.newDocument(repositoryConfiguration.getDocument());
config.getOrCreateDocument(FieldName.STORAGE).setDocument(FieldName.BINARY_STORAGE, binaryConfig);
if (config.containsField(FieldName.JOURNALING)) {
if (StringUtil.isBlank(this.journalRelativeTo)) {
this.journalRelativeTo = getDataDirectoryPathInjector().getValue();
}
if (StringUtil.isBlank(this.journalPath)) {
this.journalPath = "journal";
}
String finalJournalLocation = this.journalRelativeTo + "/" + this.journalPath;
config.getDocument(FieldName.JOURNALING).setString(FieldName.JOURNAL_LOCATION, finalJournalLocation);
}
if (LOG.isDebugEnabled()) {
LOG.debugv("ModeShape configuration for '{0}' repository: {1}", repositoryName, config);
Problems problems = repositoryConfiguration.validate();
if (!problems.isEmpty()) {
LOG.debugv("Problems with configuration for '{0}' repository: {1}", repositoryName, problems);
}
}
// Create a new (updated) configuration ...
RepositoryConfiguration updatedConfiguration = new RepositoryConfiguration(config, repositoryName);
// Deploy the repository and use this as the environment ...
engine.deploy(updatedConfiguration.with(this));
} catch (Exception e) {
throw new StartException(e);
}
}
@Override
public void stop( StopContext context ) {
ModeShapeEngine engine = getEngine();
if (engine != null) {
try {
// Undeploy the repository ...
engine.undeploy(repositoryName());
} catch (NoSuchRepositoryException e) {
// The repository doesn't exist, so no worries ...
}
}
}
/**
* Immediately change and apply the specified field in the current repository configuration to the new value.
*
* @param defn the attribute definition for the value; may not be null
* @param newValue the new string value
* @throws RepositoryException if there is a problem obtaining the repository configuration or applying the change
* @throws OperationFailedException if there is a problem obtaining the raw value from the supplied model node
*/
public void changeField( MappedAttributeDefinition defn,
ModelNode newValue ) throws RepositoryException, OperationFailedException {
ModeShapeEngine engine = getEngine();
String repositoryName = repositoryName();
// Get a snapshot of the current configuration ...
RepositoryConfiguration config = engine.getRepositoryConfiguration(repositoryName);
// Now start to make changes ...
Editor editor = config.edit();
// Find the Document containing the field ...
EditableDocument fieldContainer = editor;
for (String fieldName : defn.getPathToContainerOfField()) {
fieldContainer = editor.getOrCreateDocument(fieldName);
}
// Get the raw value from the model node ...
Object rawValue = defn.getTypedValue(newValue);
// Change the field ...
String fieldName = defn.getFieldName();
fieldContainer.set(fieldName, rawValue);
// Apply the changes to the current configuration ...
Changes changes = editor.getChanges();
engine.update(repositoryName, changes);
}
/**
* Immediately change and apply the specified index provider field in the current repository configuration to the new value.
*
* @param defn the attribute definition for the value; may not be null
* @param newValue the new string value
* @param indexProviderName the name of the index provider
* @throws RepositoryException if there is a problem obtaining the repository configuration or applying the change
* @throws OperationFailedException if there is a problem obtaining the raw value from the supplied model node
*/
public void changeIndexProviderField( MappedAttributeDefinition defn,
ModelNode newValue,
String indexProviderName ) throws RepositoryException, OperationFailedException {
ModeShapeEngine engine = getEngine();
String repositoryName = repositoryName();
// Get a snapshot of the current configuration ...
RepositoryConfiguration config = engine.getRepositoryConfiguration(repositoryName);
// Now start to make changes ...
Editor editor = config.edit();
// Find the array of sequencer documents ...
List<String> pathToContainer = defn.getPathToContainerOfField();
EditableDocument providers = editor.getOrCreateDocument(pathToContainer.get(0));
// The container should be an array ...
for (String configuredProviderName : providers.keySet()) {
// Look for the entry with a name that matches our sequencer name ...
if (indexProviderName.equals(configuredProviderName)) {
// All these entries should be nested documents ...
EditableDocument provider = (EditableDocument)providers.get(configuredProviderName);
// Change the field ...
String fieldName = defn.getFieldName();
// Get the raw value from the model node ...
Object rawValue = defn.getTypedValue(newValue);
// And update the field ...
provider.set(fieldName, rawValue);
break;
}
}
// Get and apply the changes to the current configuration. Note that the 'update' call asynchronously
// updates the configuration, and returns a Future<JcrRepository> that we could use if we wanted to
// wait for the changes to take place. But we don't want/need to wait, so we'll not use the Future ...
Changes changes = editor.getChanges();
engine.update(repositoryName, changes);
}
/**
* Immediately change and apply the specified index definition field in the current repository configuration to the new value.
*
* @param defn the attribute definition for the value; may not be null
* @param newValue the new string value
* @param indexDefinitionName the name of the index definition
* @throws RepositoryException if there is a problem obtaining the repository configuration or applying the change
* @throws OperationFailedException if there is a problem obtaining the raw value from the supplied model node
*/
public void changeIndexDefinitionField( MappedAttributeDefinition defn,
ModelNode newValue,
String indexDefinitionName ) throws RepositoryException, OperationFailedException {
ModeShapeEngine engine = getEngine();
String repositoryName = repositoryName();
// Get a snapshot of the current configuration ...
RepositoryConfiguration config = engine.getRepositoryConfiguration(repositoryName);
// Now start to make changes ...
Editor editor = config.edit();
// Find the array of sequencer documents ...
List<String> pathToContainer = defn.getPathToContainerOfField();
EditableDocument indexes = editor.getOrCreateDocument(pathToContainer.get(0));
// The container should be an array ...
for (String configuredIndexName : indexes.keySet()) {
// Look for the entry with a name that matches our sequencer name ...
if (indexDefinitionName.equals(configuredIndexName)) {
// All these entries should be nested documents ...
EditableDocument provider = (EditableDocument)indexes.get(configuredIndexName);
// Change the field ...
String fieldName = defn.getFieldName();
// Get the raw value from the model node ...
Object rawValue = defn.getTypedValue(newValue);
// And update the field ...
provider.set(fieldName, rawValue);
break;
}
}
// Get and apply the changes to the current configuration. Note that the 'update' call asynchronously
// updates the configuration, and returns a Future<JcrRepository> that we could use if we wanted to
// wait for the changes to take place. But we don't want/need to wait, so we'll not use the Future ...
Changes changes = editor.getChanges();
engine.update(repositoryName, changes);
}
/**
* Immediately change and apply the specified sequencer field in the current repository configuration to the new value.
*
* @param defn the attribute definition for the value; may not be null
* @param newValue the new string value
* @param sequencerName the name of the sequencer
* @throws RepositoryException if there is a problem obtaining the repository configuration or applying the change
* @throws OperationFailedException if there is a problem obtaining the raw value from the supplied model node
*/
public void changeSequencerField( MappedAttributeDefinition defn,
ModelNode newValue,
String sequencerName ) throws RepositoryException, OperationFailedException {
ModeShapeEngine engine = getEngine();
String repositoryName = repositoryName();
// Get a snapshot of the current configuration ...
RepositoryConfiguration config = engine.getRepositoryConfiguration(repositoryName);
// Now start to make changes ...
Editor editor = config.edit();
// Find the array of sequencer documents ...
List<String> pathToContainer = defn.getPathToContainerOfField();
EditableDocument sequencing = editor.getOrCreateDocument(pathToContainer.get(0));
EditableDocument sequencers = sequencing.getOrCreateArray(pathToContainer.get(1));
// The container should be an array ...
for (String configuredSequencerName : sequencers.keySet()) {
// Look for the entry with a name that matches our sequencer name ...
if (sequencerName.equals(configuredSequencerName)) {
// All these entries should be nested documents ...
EditableDocument sequencer = (EditableDocument)sequencers.get(configuredSequencerName);
// Change the field ...
String fieldName = defn.getFieldName();
// Get the raw value from the model node ...
Object rawValue = defn.getTypedValue(newValue);
// And update the field ...
sequencer.set(fieldName, rawValue);
break;
}
}
// Get and apply the changes to the current configuration. Note that the 'update' call asynchronously
// updates the configuration, and returns a Future<JcrRepository> that we could use if we wanted to
// wait for the changes to take place. But we don't want/need to wait, so we'll not use the Future ...
Changes changes = editor.getChanges();
engine.update(repositoryName, changes);
}
/**
* Immediately change and apply the specified persistence field to the repository configuration
*
* @param defn the attribute definition for the value; may not be null
* @param newValue the new string value
* @throws RepositoryException if there is a problem obtaining the repository configuration or applying the change
* @throws OperationFailedException if there is a problem obtaining the raw value from the supplied model node
*/
public void changePersistenceField(MappedAttributeDefinition defn,
ModelNode newValue) throws RepositoryException, OperationFailedException {
ModeShapeEngine engine = getEngine();
String repositoryName = repositoryName();
// Get a snapshot of the current configuration ...
RepositoryConfiguration config = engine.getRepositoryConfiguration(repositoryName);
// Now start to make changes ...
Editor editor = config.edit();
EditableDocument persistence = editor.getOrCreateDocument(FieldName.STORAGE).getOrCreateDocument(FieldName.PERSISTENCE);
// Change the field ...
String fieldName = defn.getFieldName();
// Get the raw value from the model node ...
Object rawValue = defn.getTypedValue(newValue);
// And update the field ...
persistence.set(fieldName, rawValue);
// Get and apply the changes to the current configuration. Note that the 'update' call asynchronously
// updates the configuration, and returns a Future<JcrRepository> that we could use if we wanted to
// wait for the changes to take place. But we don't want/need to wait, so we'll not use the Future ...
Changes changes = editor.getChanges();
engine.update(repositoryName, changes);
}
/**
* Immediately change and apply the specified external source field in the current repository configuration to the new value.
*
* @param defn the attribute definition for the value; may not be null
* @param newValue the new string value
* @param sourceName the name of the source
* @throws RepositoryException if there is a problem obtaining the repository configuration or applying the change
* @throws OperationFailedException if there is a problem obtaining the raw value from the supplied model node
*/
public void changeSourceField( MappedAttributeDefinition defn,
ModelNode newValue,
String sourceName ) throws RepositoryException, OperationFailedException {
ModeShapeEngine engine = getEngine();
String repositoryName = repositoryName();
// Get a snapshot of the current configuration ...
RepositoryConfiguration config = engine.getRepositoryConfiguration(repositoryName);
// Now start to make changes ...
Editor editor = config.edit();
// Find the array of sequencer documents ...
EditableDocument externalSources = editor.getOrCreateDocument(FieldName.EXTERNAL_SOURCES);
EditableDocument externalSource = externalSources.getDocument(sourceName);
assert externalSource != null;
// Change the field ...
String fieldName = defn.getFieldName();
// Get the raw value from the model node ...
Object rawValue = defn.getTypedValue(newValue);
// And update the field ...
externalSource.set(fieldName, rawValue);
// Get and apply the changes to the current configuration. Note that the 'update' call asynchronously
// updates the configuration, and returns a Future<JcrRepository> that we could use if we wanted to
// wait for the changes to take place. But we don't want/need to wait, so we'll not use the Future ...
Changes changes = editor.getChanges();
engine.update(repositoryName, changes);
}
/**
* Immediately change and apply the specified extractor field in the current repository configuration to the new value.
*
* @param defn the attribute definition for the value; may not be null
* @param newValue the new string value
* @param extractorName the name of the sequencer
* @throws RepositoryException if there is a problem obtaining the repository configuration or applying the change
* @throws OperationFailedException if there is a problem obtaining the raw value from the supplied model node
*/
public void changeTextExtractorField( MappedAttributeDefinition defn,
ModelNode newValue,
String extractorName ) throws RepositoryException, OperationFailedException {
ModeShapeEngine engine = getEngine();
String repositoryName = repositoryName();
// Get a snapshot of the current configuration ...
RepositoryConfiguration config = engine.getRepositoryConfiguration(repositoryName);
// Now start to make changes ...
Editor editor = config.edit();
// Find the array of sequencer documents ...
List<String> pathToContainer = defn.getPathToContainerOfField();
EditableDocument textExtracting = editor.getOrCreateDocument(pathToContainer.get(1));
EditableDocument extractors = textExtracting.getOrCreateDocument(pathToContainer.get(2));
// The container should be an array ...
for (String configuredExtractorName : extractors.keySet()) {
// Look for the entry with a name that matches our extractor name ...
if (extractorName.equals(configuredExtractorName)) {
// All these entries should be nested documents ...
EditableDocument extractor = (EditableDocument)extractors.get(configuredExtractorName);
// Change the field ...
String fieldName = defn.getFieldName();
// Get the raw value from the model node ...
Object rawValue = defn.getTypedValue(newValue);
// And update the field ...
extractor.set(fieldName, rawValue);
break;
}
}
Changes changes = editor.getChanges();
engine.update(repositoryName, changes);
}
/**
* Immediately change and apply the specified authenticator field in the current repository configuration to the new value.
*
* @param defn the attribute definition for the value; may not be null
* @param newValue the new string value
* @param authenticatorName the name of the authenticator
* @throws RepositoryException if there is a problem obtaining the repository configuration or applying the change
* @throws OperationFailedException if there is a problem obtaining the raw value from the supplied model node
*/
public void changeAuthenticatorField( MappedAttributeDefinition defn,
ModelNode newValue,
String authenticatorName ) throws RepositoryException, OperationFailedException {
ModeShapeEngine engine = getEngine();
String repositoryName = repositoryName();
// Get a snapshot of the current configuration ...
RepositoryConfiguration config = engine.getRepositoryConfiguration(repositoryName);
// Now start to make changes ...
Editor editor = config.edit();
// Find the array of sequencer documents ...
EditableDocument security = editor.getOrCreateDocument(FieldName.SECURITY);
EditableArray providers = security.getOrCreateArray(FieldName.PROVIDERS);
// The container should be an array ...
for (String configuredAuthenticatorName : providers.keySet()) {
// Look for the entry with a name that matches our authenticator name ...
if (authenticatorName.equals(configuredAuthenticatorName)) {
// Find the document in the array with the name field value that matches ...
boolean found = false;
for (Object nested : providers) {
if (nested instanceof EditableDocument) {
EditableDocument doc = (EditableDocument)nested;
if (doc.getString(FieldName.NAME).equals(configuredAuthenticatorName)) {
// Change the field ...
String fieldName = defn.getFieldName();
// Get the raw value from the model node ...
Object rawValue = defn.getTypedValue(newValue);
// And update the field ...
doc.set(fieldName, rawValue);
found = true;
break;
}
}
}
if (!found) {
// Add the nested document ...
EditableDocument doc = Schematic.newDocument();
doc.set(FieldName.NAME, configuredAuthenticatorName);
// Set the field ...
String fieldName = defn.getFieldName();
// Get the raw value from the model node ...
Object rawValue = defn.getTypedValue(newValue);
// And update the field ...
doc.set(fieldName, rawValue);
providers.add(doc);
}
break;
}
}
Changes changes = editor.getChanges();
engine.update(repositoryName, changes);
}
/**
* @return the injector used to set the configuration details for the binaries storage
*/
public InjectedValue<BinaryStorage> getBinaryStorageInjector() {
return binaryStorageInjector;
}
/**
* @return the injector used to get the repository statistics (never <code>null</code>)
*/
public InjectedValue<RepositoryStatistics> getMonitorInjector() {
return this.monitorInjector;
}
/**
* @return the injector used to set the {@link ModeShapeEngine} reference
*/
public InjectedValue<ModeShapeEngine> getEngineInjector() {
return engineInjector;
}
/**
* @return the injector used to set the data directory for this repository
*/
public InjectedValue<String> getDataDirectoryPathInjector() {
return dataDirectoryPathInjector;
}
/**
* @return the injector used to set the jboss module loader
*/
public InjectedValue<ModuleLoader> getModuleLoaderInjector() {
return moduleLoaderInjector;
}
public InjectedValue<ISecurityManagement> getSecurityManagementServiceInjector() {
return securityManagementServiceInjector;
}
public InjectedValue<ChannelFactory> getChannelFactoryInjector() {
return channelFactoryInjector;
}
private ModuleLoader moduleLoader() {
return moduleLoaderInjector.getValue();
}
/**
* Sets the path (relative) of the journal.
*
* @param journalPath a {@link String}, may not be null
*/
public void setJournalPath( String journalPath ) {
this.journalPath = journalPath;
}
/**
* Sets the base folder of the journal
*
* @param journalRelativeTo a {@link String}, may not be null
*/
public void setJournalRelativeTo( String journalRelativeTo ) {
this.journalRelativeTo = journalRelativeTo;
}
}