org.modeshape.jcr.api.sequencer.Sequencer Maven / Gradle / Ivy
Show all versions of modeshape-jcr-api Show documentation
/*
* 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.api.sequencer;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import javax.jcr.NamespaceException;
import javax.jcr.NamespaceRegistry;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import org.modeshape.jcr.api.Logger;
import org.modeshape.jcr.api.nodetype.NodeTypeManager;
/**
* A component that reads recently-changed content (often uploaded files) and extracts additional information from the content.
*
* Each ModeShape repository can be configured with zero or more sequencers. Each sequencer is configured with a set of match
* conditions that define the acceptable patterns for the paths of changed nodes, as well as a path specification that defines
* where the derived (or generated) output should be placed. Then when clients change nodes with paths that satisfy the match
* conditions, the repository will create a new Session and invoke the sequencer, which is then expected to process the changed
* content and generate the derived information under the supplied parent node. The session will be saved automatically or, if an
* exception is thrown, discard the changes and close the session.
*
*/
public abstract class Sequencer {
private final UUID uuid = UUID.randomUUID();
/**
* The logger instance, set via reflection
*/
private Logger logger;
/**
* The name of this sequencer, set via reflection
*/
private String name;
/**
* The name of the repository that owns this sequencer, set via reflection
*/
private String repositoryName;
/**
* The multiple path expressions for this sequencer, set via reflection
*/
private Object[] pathExpressions;
/**
* The singular path expression of this sequencer, set via reflection
*/
private String pathExpression;
/**
* The set of MIME types that this sequencer will process. Subclasses should set call
* {@link #registerDefaultMimeTypes(String...)} in the no-arg constructor to set the default MIME types for the sequencer, but
* the field may be overwritten in the sequencer's configuration by setting the "acceptedMimeTypes" field to an array of
* string values.
*/
private String[] acceptedMimeTypes = {};
private Set acceptedMimeTypesSet = null;
private boolean initialized = false;
/**
* Return the unique identifier for this sequencer.
*
* @return the unique identifier; never null
*/
public final UUID getUniqueId() {
return uuid;
}
/**
* Get the name for this sequencer.
*
* @return the name, or null if there is no description
*/
public final String getName() {
return name;
}
/**
* Get the name of the repository.
*
* @return the repository name; never null
*/
public final String getRepositoryName() {
return repositoryName;
}
/**
* Obtain the path expressions as configured on the sequencer. This method always returns a copy to prevent modification of
* the values.
*
* @return the path expressions; never null but possibly empty
*/
public final String[] getPathExpressions() {
String pathExpression = this.pathExpression;
Object[] pathExpressions = this.pathExpressions;
if (pathExpression == null && (pathExpressions == null || pathExpressions.length == 0)) {
// there's none ...
return new String[] {};
}
if (pathExpression != null && (pathExpressions == null || pathExpressions.length == 0)) {
// There's just one ...
return new String[] {pathExpression};
}
List expressions = new ArrayList(pathExpressions.length + 1);
addExpression(expressions, pathExpression);
for (Object value : pathExpressions) {
addExpression(expressions, value);
}
return expressions.toArray(new String[expressions.size()]);
}
private void addExpression( List values,
Object value ) {
assert !initialized : "No expressions can be added after the sequencer has been initialized";
if (value instanceof String) {
String str = (String)value;
str = str.trim();
if (str.length() != 0) {
values.add(str);
}
}
}
/**
* Initialize the sequencer. This is called automatically by ModeShape once for each Sequencer instance, and should not be
* called by the sequencer.
*
* By default this method does nothing, so it should be overridden by implementations to do a one-time initialization of any
* internal components. For example, sequencers can use the supplied registry
and nodeTypeManager
* objects to register custom namesapces and node types required by the generated content.
*
*
* @param registry the namespace registry that can be used to register custom namespaces; never null
* @param nodeTypeManager the node type manager that can be used to register custom node types; never null
* @throws RepositoryException if operations on the {@link NamespaceRegistry} or {@link NodeTypeManager} fail
* @throws IOException if any stream based operations fail (like importing cnd files)
*/
public void initialize( NamespaceRegistry registry,
NodeTypeManager nodeTypeManager ) throws RepositoryException, IOException {
// Subclasses may not necessarily call 'super.initialize(...)', but if they do then we can make this assertion ...
assert !initialized : "The Sequencer.initialize(...) method should not be called by subclasses; ModeShape has already (and automatically) initialized the Sequencer";
}
/**
* Method called by the code calling {@link #initialize(NamespaceRegistry, NodeTypeManager)} (typically via reflection) to
* signal that the initialize method is completed. See Sequencers.initialize() for details, and no this method is indeed used.
*/
@SuppressWarnings( "unused" )
private void postInitialize() {
if (!initialized) {
initialized = true;
// ------------------------------------------------------------------------------------------------------------
// Add any code here that needs to run after #initialize(...), which will be overwritten by subclasses
// ------------------------------------------------------------------------------------------------------------
// Make immutable the Set of accepts MIME types ...
acceptedMimeTypesSet = Collections.unmodifiableSet(getAcceptedMimeTypes());
}
}
/**
* Execute the sequencing operation on the specified property, which has recently been created or changed.
*
* Each sequencer is expected to process the value of the property, extract information from the value, and write a structured
* representation (in the form of a node or a subgraph of nodes) using the supplied output node. Note that the output node
* will either be:
*
* - the selected node, in which case the sequencer was configured to generate the output information directly under the
* selected input node; or
* - a newly created node in a different location than node being sequenced (in this case, the primary type of the new node
* will be 'nt:unstructured', but the sequencer can easily change that using {@link Node#setPrimaryType(String)})
*
*
*
* The implementation is expected to always clean up all resources that it acquired, even in the case of exceptions.
*
*
* Note: This method must be threadsafe: ModeShape will likely invoke this method concurrently in separate threads,
* and the method should never modify the state or fields of the Sequencer implementation class. All initialization should be
* performed in {@link #initialize(NamespaceRegistry, NodeTypeManager)}.
*
*
* @param inputProperty the property that was changed and that should be used as the input; never null
* @param outputNode the node that represents the output for the derived information; never null, and will either be
* {@link Node#isNew() new} if the output is being placed outside of the selected node, or will not be new when the
* output is to be placed on the selected input node
* @param context the context in which this sequencer is executing, and which may contain additional parameters useful when
* generating the output structure; never null
* @return true if the sequencer's output should be saved, or false otherwise
* @throws Exception if there was a problem with the sequencer that could not be handled. All exceptions will be logged
* automatically as errors by ModeShape.
*/
public abstract boolean execute( Property inputProperty,
Node outputNode,
Context context ) throws Exception;
@Override
public String toString() {
return repositoryName + " -> " + getClass().getName() + " uuid=" + uuid + (name != null ? (" : " + name) : "");
}
/**
* Registers a namespace using the given {@link NamespaceRegistry}, if the namespace has not been previously registered.
*
* @param namespacePrefix a non-null {@code String}
* @param namespaceUri a non-null {@code String}
* @param namespaceRegistry a {@code NamespaceRegistry} instance.
* @return true if the namespace has been registered, or false if it was already registered
* @throws RepositoryException if anything fails during the registration process
*/
protected boolean registerNamespace( String namespacePrefix,
String namespaceUri,
NamespaceRegistry namespaceRegistry ) throws RepositoryException {
if (namespacePrefix == null || namespaceUri == null) {
throw new IllegalArgumentException("Neither the namespace prefix, nor the uri should be null");
}
try {
// if the call succeeds, means it was previously registered
namespaceRegistry.getPrefix(namespaceUri);
return false;
} catch (NamespaceException e) {
// namespace not registered yet
namespaceRegistry.registerNamespace(namespacePrefix, namespaceUri);
return true;
}
}
/**
* Registers node types from a CND file, using the given {@link NodeTypeManager}. Any namespaces defined in the CND file will
* be automatically registered as well.
*
* @param cndFile the relative path to the cnd file, which is loaded using via {@link Class#getResourceAsStream(String)}
* @param nodeTypeManager the node type manager with which the cnd will be registered
* @param allowUpdate a boolean which indicates whether updates on existing node types are allowed or no. See
* {@link NodeTypeManager#registerNodeType(javax.jcr.nodetype.NodeTypeDefinition, boolean)}
* @throws RepositoryException if anything fails
* @throws IOException if any stream related operations fail
*/
protected void registerNodeTypes( String cndFile,
NodeTypeManager nodeTypeManager,
boolean allowUpdate ) throws RepositoryException, IOException {
InputStream cndStream = getClass().getResourceAsStream(cndFile);
registerNodeTypes(cndStream, nodeTypeManager, allowUpdate);
}
/**
* See {@link Sequencer#registerNodeTypes(String, org.modeshape.jcr.api.nodetype.NodeTypeManager, boolean)}
*
* @param cndStream the input stream containing the CND file; may not be null
* @param nodeTypeManager the node type manager with which the node types in the CND file should be registered; may not be
* null
* @param allowUpdate a boolean which indicates whether updates on existing node types are allowed or no. See
* {@link NodeTypeManager#registerNodeType(javax.jcr.nodetype.NodeTypeDefinition, boolean)}
* @throws RepositoryException if anything fails
* @throws IOException if any stream related operations fail
*/
protected void registerNodeTypes( InputStream cndStream,
NodeTypeManager nodeTypeManager,
boolean allowUpdate ) throws RepositoryException, IOException {
if (cndStream == null) {
throw new IllegalArgumentException("The stream to the given cnd file is null");
}
nodeTypeManager.registerNodeTypes(cndStream, allowUpdate);
}
protected final Logger getLogger() {
return logger;
}
/**
* Set the MIME types that are accepted by default, if there are any. This method should be called from the
* {@link #initialize(NamespaceRegistry, NodeTypeManager)} method in the subclass.
*
* This method can be called more than once to add additional mime types.
*
*
* @param mimeTypes the array of MIME types that are accepted by this sequencer
* @see #isAccepted(String)
*/
protected final void registerDefaultMimeTypes( String... mimeTypes ) {
assert !initialized : "No default MIME types can be registered after the sequencer has been initialized";
if (mimeTypes != null && mimeTypes.length != 0 && acceptedMimeTypes.length == 0) {
// There are no overridden mime types, so we can register the default MIME types ...
if (acceptedMimeTypesSet == null) acceptedMimeTypesSet = new HashSet();
for (String mimeType : mimeTypes) {
if (mimeType == null) continue;
mimeType = mimeType.trim();
if (mimeType.length() == 0) continue;
acceptedMimeTypesSet.add(mimeType);
}
}
}
/**
* Utility method to obtain the set of accepted MIME types. The resulting set will either be those set by default in the
* subclass' overridden {@link #initialize(NamespaceRegistry, NodeTypeManager)} method or the MIME types explicitly set in the
* sequencers configuration.
*
* @return the set of MIME types that are accepted by this Sequencer instance; never null but possibly empty if this Sequencer
* instance accepts all MIME types
*/
protected final Set getAcceptedMimeTypes() {
if (acceptedMimeTypesSet == null) {
// No defaults are registered, so use those non-defaults ...
acceptedMimeTypesSet = new HashSet();
for (String mimeType : acceptedMimeTypes) {
if (mimeType == null) continue;
mimeType = mimeType.trim();
if (mimeType.length() == 0) continue;
acceptedMimeTypesSet.add(mimeType);
}
}
return acceptedMimeTypesSet;
}
/**
* Determine if this sequencer requires the content to have a specific MIME type
*
* @return true if this sequencer can only process certain MIME types, or false if there are no restrictions
*/
public final boolean hasAcceptedMimeTypes() {
return !getAcceptedMimeTypes().isEmpty();
}
/**
* Determine if this sequencer has been configured to accept and process content with the supplied MIME type.
*
* @param mimeType the MIME type
* @return true if content with the supplied the MIME type is to be processed (or when mimeType
is null and
* therefore not known), or false otherwise
* @see #hasAcceptedMimeTypes()
*/
public final boolean isAccepted( String mimeType ) {
if (mimeType != null && hasAcceptedMimeTypes()) {
return getAcceptedMimeTypes().contains(mimeType.trim());
}
return true; // accept all mime types
}
/**
* The sequencer context represents the complete context of a sequencer invocation. Currently, this information includes the
* current time of sequencer execution.
*/
public interface Context {
/**
* Get the timestamp of the sequencing. This is always the timestamp of the change event that is being processed.
*
* @return timestamp the "current" timestamp; never null
*/
Calendar getTimestamp();
/**
* Returns a {@link org.modeshape.jcr.api.ValueFactory} instance which can be used to perform additional type conversions,
* from what {@link javax.jcr.ValueFactory} offers
*
* @return a non-null value factory, using the output node's session as context
*/
org.modeshape.jcr.api.ValueFactory valueFactory();
}
}