All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.modeshape.jcr.spi.federation.Connector Maven / Gradle / Ivy

There is a newer version: 5.4.1.Final
Show newest version
/*
 * 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.spi.federation;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import javax.jcr.NamespaceRegistry;
import javax.jcr.RepositoryException;
import javax.transaction.TransactionManager;
import org.infinispan.schematic.DocumentFactory;
import org.infinispan.schematic.document.Document;
import org.modeshape.common.logging.Logger;
import org.modeshape.common.text.TextDecoder;
import org.modeshape.common.util.CheckArg;
import org.modeshape.jcr.Environment;
import org.modeshape.jcr.ExecutionContext;
import org.modeshape.jcr.JcrI18n;
import org.modeshape.jcr.JcrLexicon;
import org.modeshape.jcr.api.nodetype.NodeTypeManager;
import org.modeshape.jcr.cache.DocumentAlreadyExistsException;
import org.modeshape.jcr.cache.DocumentNotFoundException;
import org.modeshape.jcr.cache.document.DocumentTranslator;
import org.modeshape.jcr.federation.FederatedDocumentReader;
import org.modeshape.jcr.federation.FederatedDocumentWriter;
import org.modeshape.jcr.mimetype.MimeTypeDetector;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.NameFactory;
import org.modeshape.jcr.value.Path;
import org.modeshape.jcr.value.PathFactory;
import org.modeshape.jcr.value.Property;
import org.modeshape.jcr.value.PropertyFactory;
import org.modeshape.jcr.value.ValueFactories;
import org.modeshape.jcr.value.ValueFormatException;
import org.modeshape.jcr.value.binary.ExternalBinaryValue;
import org.modeshape.jcr.value.binary.UrlBinaryValue;

/**
 * SPI of a generic external connector, representing the interface to an external system integrated with ModeShape. Since it is
 * expected that the documents are well formed (structure-wise), the {@link FederatedDocumentWriter} class should be used. This is
 * the base class for {@link WritableConnector} and {@link ReadOnlyConnector} which is what connector implementations are expected
 * to implement.
 *
 * @author Horia Chiorean ([email protected])
 */
public abstract class Connector {

    /**
     * The logger instance, set via reflection
     *
     * @see #simpleLogger
     */
    private Logger logger;

    /**
     * The simpler API logger instance, set via reflection
     *
     * @see #logger
     */
    private org.modeshape.jcr.api.Logger simpleLogger;

    /**
     * The name of this connector, set via reflection immediately after instantiation.
     */
    private String name;

    /**
     * The name of the repository that owns this connector, set via reflection immediately after instantiation.
     */
    private String repositoryName;

    /**
     * The execution context, set via reflection before ModeShape calls {@link #initialize(NamespaceRegistry, NodeTypeManager)}.
     */
    private ExecutionContext context;

    /**
     * The MIME type detector, set via reflection before ModeShape calls {@link #initialize(NamespaceRegistry, NodeTypeManager)}.
     */
    private MimeTypeDetector mimeTypeDetector;

    /**
     * The default maximum number of seconds that a document returned by this connector should be stored in the workspace cache.
     * This can be overwritten, on a per-document-basis.
     * 

* The field is assigned via reflection based upon the configuration of the external source represented by this connector * before ModeShape calls {@link #initialize(NamespaceRegistry, NodeTypeManager)}. *

*/ private Integer cacheTtlSeconds; /** * A flag which indicates whether content exposed by this connector should be indexed or not by the repository. This acts as a * global flag, allowing a connector to mark it's entire content as non-queryable. By default, all content is queryable. *

* The field is assigned via reflection based upon the configuration of the external source represented by this connector * before ModeShape calls {@link #initialize(NamespaceRegistry, NodeTypeManager)}. *

*/ private boolean queryable = true; private boolean initialized = false; /** * A document translator that is used within the DocumentReader implementation, but which has no DocumentStore reference and * thus is not fully-functional. *

* The field is assigned via reflection before ModeShape calls {@link #initialize(NamespaceRegistry, NodeTypeManager)}. *

*/ private DocumentTranslator translator; /** * A property store that the connector can use to persist "extra" properties that cannot be stored in the external system. The * use of this store is optional, and connectors should store as much information as possible in the external system. * Connectors are also responsible for removing the extra properties for a node when it is removed. *

* The field is assigned via reflection before ModeShape calls {@link #initialize(NamespaceRegistry, NodeTypeManager)}. *

*/ private ExtraPropertiesStore extraPropertiesStore; /** * The {@link TransactionManager} instance used by the repository, which allows a connector to enroll, via an * {@link javax.transaction.xa.XAResource} implementation into existing transactions. It's up to the connector to implement * the logic for determining transaction boundaries and to process documents accordingly. *

* The field is assigned via reflection before ModeShape calls {@link #initialize(NamespaceRegistry, NodeTypeManager)}. *

*/ private TransactionManager transactionManager; /** * The {@link ConnectorChangeSetFactory} instance which allows a connector to create {@link ConnectorChangeSet}s. *

* The field is assigned via reflection before ModeShape calls {@link #initialize(NamespaceRegistry, NodeTypeManager)}. *

*/ private ConnectorChangeSetFactory connectorChangedSetFactory; /** * The {@link Environment} instance in which the repository operates. This should be used by connectors which need to perform * class-path related operations. *

* The field is assigned via reflection before ModeShape calls {@link #initialize(NamespaceRegistry, NodeTypeManager)}. *

*/ private Environment environment; /** * Ever connector is expected to have a no-argument constructor, although the class should never initialize any of the data at * this time. Instead, all initialization should be performed in the {@link #initialize} method. */ public Connector() { } /** * Returns the name of the source which this connector interfaces with. * * @return a {@code non-null} string. */ public String getSourceName() { return name; } /** * Get the name of the repository. * * @return the repository name; never null */ public final String getRepositoryName() { return repositoryName; } /** * Get the I18n-compatible logger for this connector instance. This is available for use in or after the * {@link #initialize(NamespaceRegistry, NodeTypeManager)} method. *

* This logger is a bit more complicated than the {@link #log() simple logger} because it requires use of ModeShape's * internationalization and localization framework. All of ModeShape's code uses this I18n framework, making it much easier to * localize ModeShape to other languages. Using this logger would make it just as easy to localize the Connector * implementation. But since it is more complicated, it may not be for every developer. Subclasses can use this logger or the * simpler logger; you can even mix and match (though we'd recommend that you just pick one and only use it). *

* * @return the logger that requires using ModeShape I18n framework; never null * @see #log() */ public final Logger getLogger() { return logger; } /** * Get the simpler, String-based logger for this connector instance. This is available for use in or after the * {@link #initialize(NamespaceRegistry, NodeTypeManager)} method. *

* This logger is much simpler than the {@link #getLogger() I18n-compatible logger}, especially since all of the log messages * take simple strings. However, this simple logger is not easily internationalized. Subclasses can use this logger or the * more formal, I18n-compatible logger; you can even mix and match (though we'd recommend that you just pick one and only use * it). *

* * @return the simple logger that requires String messages; never null * @see #getLogger() */ public final org.modeshape.jcr.api.Logger log() { return simpleLogger; } /** * Get the execution context for this connector instance. This is available for use in or after the * {@link #initialize(NamespaceRegistry, NodeTypeManager)} method. * * @return the context; never null */ public ExecutionContext getContext() { return context; } /** * Get the MIME type detector for this connector instance. This is available for use in or after the * {@link #initialize(NamespaceRegistry, NodeTypeManager)} method. * * @return the MIME type detector; never null */ public MimeTypeDetector getMimeTypeDetector() { return mimeTypeDetector; } /** * Returns the default value, for this connector, of the maximum number of seconds an external document should be stored in * the workspace cache. * * @return an {@link Integer} value. If {@code null}, it means that no special value is configured and the default workspace * cache configuration will be used. If negative, it means an entry will be cached forever. */ public Integer getCacheTtlSeconds() { return cacheTtlSeconds; } /** * Indicates if content exposed by this connector should be indexed by the repository or not. * * @return {@code true} if the content should be indexed, {@code false} otherwise. */ public Boolean isQueryable() { return queryable; } protected ExtraProperties extraPropertiesFor( String id, boolean update ) { return new ExtraProperties(id, update); } /** * Get the "extra" properties store. Connectors can directly use this, although it's probably easier to * {@link #extraPropertiesFor(String,boolean) create} an {@link ExtraProperties} object for each node and use it to * {@link ExtraProperties#add(Property) add}, {@link ExtraProperties#remove(Name) remove} and then * {@link ExtraProperties#save() store or update} the extra properties in the extra properties store. * * @return the storage for extra properties; never null */ protected ExtraPropertiesStore extraPropertiesStore() { return extraPropertiesStore; } /** * Method that can be called by a connector during {@link #initialize(NamespaceRegistry, NodeTypeManager) initialization} if * it wants to provide its own implementation of an "extra" properties store. * * @param customExtraPropertiesStore the custom implementation of the ExtraPropertiesStore; may not be null */ protected void setExtraPropertiesStore( ExtraPropertiesStore customExtraPropertiesStore ) { CheckArg.isNotNull(customExtraPropertiesStore, "customExtraPropertiesStore"); this.extraPropertiesStore = customExtraPropertiesStore; } /** * Moves a set of extra properties from an old to a new node after their IDs have changed. * * @param oldNodeId the old identifier for the node; may not be null * @param newNodeId the new identifier for the node; may not be null */ protected void moveExtraProperties( String oldNodeId, String newNodeId ) { ExtraPropertiesStore extraPropertiesStore = extraPropertiesStore(); if (extraPropertiesStore == null || !extraPropertiesStore.contains(oldNodeId)) { return; } Map existingExtraProps = extraPropertiesStore.getProperties(oldNodeId); extraPropertiesStore.removeProperties(oldNodeId); extraPropertiesStore.storeProperties(newNodeId, existingExtraProps); } /** * Returns the transaction manager instance that was set on the connector during initialization. * * @return a {@code non-null} {@link TransactionManager} instance */ protected TransactionManager getTransactionManager() { return transactionManager; } /** * Returns the repository environment that was set during connector initialization * * @return a {@code non-null} {@link Environment} instace. */ protected Environment getEnvironment() { return environment; } /** * Initialize the connector. This is called automatically by ModeShape once for each Connector instance, and should not be * called by the connector. By the time this method is called, ModeShape will hav already set the {@link #context}, * {@link #logger}, {@link #name}, and {@link #repositoryName} plus any fields that match configuration properties for the * connector. *

* 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, connectors can use the supplied registry and nodeTypeManager * objects to register custom namesapces and node types required by the external content. *

*

* This is an excellent place for connector to validate the connector-specific fields set by ModeShape via reflection during * instantiation. *

* * @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 Connector.initialize(...) method should not be called by subclasses; ModeShape has already (and automatically) initialized the Connector"; } /** * Method called by the code calling {@link #initialize} (typically via reflection) to signal that the initialize method is * completed. See 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 // ------------------------------------------------------------------------------------------------------------ } } /** * Shutdown the connector by releasing all resources. This is called automatically by ModeShape when this Connector instance * is no longer needed, and should never be called by the connector. */ public void shutdown() { // do nothing by default } /** * Returns a {@link Document} instance representing the document with a given id. The document should have a "proper" * structure for it to be usable by ModeShape. * * @param id a {@code non-null} string * @return either an {@link Document} instance or {@code null} */ public abstract Document getDocumentById( String id ); /** * Returns the id of an external node located at the given external path within the connector's exposed tree of content. * * @param externalPath a {@code non-null} string representing an external path, or "/" for the top-level node exposed by the * connector * @return either the id of the document or {@code null} */ public abstract String getDocumentId( String externalPath ); /** * Return the path(s) of the external node with the given identifier. The resulting paths are from the point of view of the * connector. For example, the "root" node exposed by the connector wil have a path of "/". * * @param id a {@code non-null} string * @return the connector-specific path(s) of the node, or an empty document if there is no such document; never null */ public abstract Collection getDocumentPathsById( String id ); /** * Indicates if the connector instance has been configured in read-only mode. * * @return {@code true} if the connector has been configured in read-only mode, false otherwise. */ public abstract boolean isReadonly(); /** * Returns a document representing a single child reference from the supplied parent to the supplied child. This method is * called when there are an unknown number of children on a node. *

* This method should be implemented and will be called if and only if a {@link Pageable connector uses paging} and specifies * an {@link PageWriter#UNKNOWN_TOTAL_SIZE unknown number of children} in the * {@link PageWriter#addPage(String, int, long, long)} or {@link PageWriter#addPage(String, String, long, long)} methods. *

* * @param parentKey the key for the parent * @param childKey the key for the child * @return the document representation of a child reference, of null if the parent does not contain a child with the given key */ public Document getChildReference( String parentKey, String childKey ) { return null; } /** * Returns a binary value which is connector specific and which is never stored by ModeShape. Connectors who need this feature * must return an object that is an instance of a subclasses of {@link ExternalBinaryValue}, either {@link UrlBinaryValue} or * a custom subclass with connector-specific information. *

* Normally, the {@link #getDocumentById(String)} method implementation will set binary values on properties of nodes, which * should create the same ExternalBinaryValue subclass that is returned by this method. The * {@link ExternalBinaryValue#getId()} value from that instance will be passed into this method. *

* * @param id a {@code String} representing the identifier of the external binary which should have connector-specific meaning. * This identifier need not be the SHA-1 hash of the content. * @return either a binary value implementation or {@code null} if there is no such value with the given id. */ public ExternalBinaryValue getBinaryValue( String id ) { return null; } /** * Removes the document with the given id. * * @param id a {@code non-null} string. * @return true if the document was removed, or false if there was no document with the given id */ public abstract boolean removeDocument( String id ); /** * Checks if a document with the given id exists in the end-source. * * @param id a {@code non-null} string. * @return {@code true} if such a document exists, {@code false} otherwise. */ public abstract boolean hasDocument( String id ); /** * Stores the given document. * * @param document a {@code non-null} {@link org.infinispan.schematic.document.Document} instance. * @throws DocumentAlreadyExistsException if there is already a new document with the same identifier * @throws DocumentNotFoundException if one of the modified documents was removed by another session */ public abstract void storeDocument( Document document ); /** * Updates a document using the provided changes. * * @param documentChanges a {@code non-null} {@link DocumentChanges} object which contains granular information about all the * changes. */ public abstract void updateDocument( DocumentChanges documentChanges ); /** * Generates an identifier which will be assigned when a new document (aka. child) is created under an existing document * (aka.parent). This method should be implemented only by connectors which support writing. * * @param parentId a {@code non-null} {@link String} which represents the identifier of the parent under which the new * document will be created. * @param newDocumentName a {@code non-null} {@link org.modeshape.jcr.value.Name} which represents the name that will be given * to the child document * @param newDocumentPrimaryType a {@code non-null} {@link org.modeshape.jcr.value.Name} which represents the child document's * primary type. * @return either a {@code non-null} {@link String} which will be assigned as the new identifier, or {@code null} which means * that no "special" id format is required. In this last case, the repository will auto-generate a random id. * @throws org.modeshape.jcr.cache.DocumentStoreException if the connector is readonly. */ public abstract String newDocumentId( String parentId, Name newDocumentName, Name newDocumentPrimaryType ); /** * Utility method that checks whether the field with the supplied name is set. * * @param fieldValue the value of the field * @param fieldName the name of the field * @throws RepositoryException if the field value is null */ protected void checkFieldNotNull( Object fieldValue, String fieldName ) throws RepositoryException { if (fieldValue == null) { throw new RepositoryException(JcrI18n.requiredFieldNotSetInConnector.text(getSourceName(), getClass(), fieldName)); } } protected DocumentTranslator translator() { return translator; } /** * Obtain a new {@link DocumentReader} that can be used to read an existing document, typically used within the * {@link #storeDocument(Document)} and {@link #updateDocument(DocumentChanges)} methods. * * @param document the document that should be read; may not be null * @return the document reader; never null */ protected DocumentReader readDocument( Document document ) { return new FederatedDocumentReader(translator, document); } /** * Obtain a new {@link DocumentWriter} that can be used to construct a document, typically within the * {@link #getDocumentById(String)} method. * * @param id the identifier of the document; may not be null * @return the document writer; never null */ protected DocumentWriter newDocument( String id ) { return new FederatedDocumentWriter(translator).setId(id); } /** * Obtain a new {@link DocumentWriter} that can be used to update a document. * * @param document the document that should be updated; may not be null * @return the document writer; never null */ protected DocumentWriter writeDocument( Document document ) { return new FederatedDocumentWriter(translator, document); } /** * Obtain a new {@link PageWriter} that can be used to construct a page of children, typically within the * {@link Pageable#getChildren(PageKey)} method. * * @param pageKey the key for the page; may not be null * @return the page writer; never null */ protected PageWriter newPageDocument( PageKey pageKey ) { return new FederatedDocumentWriter(translator).setId(pageKey.toString()); } /** * Obtain a new child reference document that is useful in the {@link #getChildReference(String, String)} method. * * @param childId the ID of the child node; may not be null * @param childName the name of the child node; may not be null * @return the child reference document; never null */ protected Document newChildReference( String childId, String childName ) { return DocumentFactory.newDocument(DocumentTranslator.KEY, childId, DocumentTranslator.NAME, childName); } /** * Get the set of value factory objects that the connector can use to create property value objects. * * @return the collection of factories; never null */ protected final ValueFactories factories() { return context.getValueFactories(); } protected final PropertyFactory propertyFactory() { return context.getPropertyFactory(); } protected final PathFactory pathFactory() { return factories().getPathFactory(); } /** * Helper method that creates a {@link Path} object from a string. This is equivalent to calling " * pathFactory().create(path)", and is simply provided for convenience. * * @param path the string from which the path is to be created * @return the value, or null if the supplied string is null * @throws ValueFormatException if the conversion from a string could not be performed * @see PathFactory#create(String) * @see #pathFrom(Path, String) */ protected final Path pathFrom( String path ) { return factories().getPathFactory().create(path); } /** * Helper method that creates a {@link Path} object from a parent path and a child path string. This is equivalent to calling * " pathFactory().create(parentPath,childPath)", and is simply provided for convenience. * * @param parentPath the parent path * @param childPath the child path as a string * @return the value, or null if the supplied string is null * @throws ValueFormatException if the conversion from a string could not be performed * @see PathFactory#create(String) * @see #pathFrom(String) */ protected final Path pathFrom( Path parentPath, String childPath ) { Path parent = pathFactory().create(parentPath); return pathFactory().create(parent, childPath); } /** * Helper method that creates a {@link Name} object from a string, using no decoding. This is equivalent to calling " * factories().getNameFactory().create(nameString)", and is simply provided for convenience. * * @param nameString the string from which the name is to be created * @return the value, or null if the supplied string is null * @throws ValueFormatException if the conversion from a string could not be performed * @see NameFactory#create(String, TextDecoder) * @see NameFactory#create(String, String, TextDecoder) * @see NameFactory#create(String, String) * @see #nameFrom(String, String) * @see #nameFrom(String, String, TextDecoder) */ protected final Name nameFrom( String nameString ) { return factories().getNameFactory().create(nameString); } /** * Create a name from the given namespace URI and local name. This is equivalent to calling " * factories().getNameFactory().create(namespaceUri,localName)", and is simply provided for convenience. * * @param namespaceUri the namespace URI * @param localName the local name * @return the new name * @throws IllegalArgumentException if the local name is null or empty * @see NameFactory#create(String, TextDecoder) * @see NameFactory#create(String, String, TextDecoder) * @see NameFactory#create(String, String) * @see #nameFrom(String) * @see #nameFrom(String, String, TextDecoder) */ protected final Name nameFrom( String namespaceUri, String localName ) { return factories().getNameFactory().create(namespaceUri, localName); } /** * Create a name from the given namespace URI and local name. This is equivalent to calling " * factories().getNameFactory().create(namespaceUri,localName,decoder)", and is simply provided for convenience. * * @param namespaceUri the namespace URI * @param localName the local name * @param decoder the decoder that should be used to decode the qualified name * @return the new name * @throws IllegalArgumentException if the local name is null or empty * @see NameFactory#create(String, String, TextDecoder) * @see NameFactory#create(String, TextDecoder) * @see NameFactory#create(String, String, TextDecoder) * @see NameFactory#create(String, String) * @see #nameFrom(String) * @see #nameFrom(String, String) */ protected final Name nameFrom( String namespaceUri, String localName, TextDecoder decoder ) { return factories().getNameFactory().create(namespaceUri, localName, decoder); } /** * @return a fresh {@link ConnectorChangeSet} for use in recording changes */ protected ConnectorChangeSet newConnectorChangedSet() { return connectorChangedSetFactory.newChangeSet(); } public final class ExtraProperties { private Map properties = new HashMap(); private final boolean update; private final String id; protected ExtraProperties( String id, boolean update ) { this.id = id; this.update = update; } public ExtraProperties add( Property property ) { this.properties.put(property.getName(), property); return this; } public ExtraProperties addAll( Map properties ) { this.properties.putAll(properties); return this; } public ExtraProperties remove( Name propertyName ) { this.properties.put(propertyName, null); return this; } public ExtraProperties remove( String propertyName ) { this.properties.put(nameFrom(propertyName), null); return this; } public ExtraProperties except( Name... names ) { for (Name name : names) { this.properties.remove(name); } return this; } public ExtraProperties except( String... names ) { for (String name : names) { this.properties.remove(nameFrom(name)); } return this; } public ExtraProperties exceptPrimaryType() { this.properties.remove(JcrLexicon.PRIMARY_TYPE); return this; } public void save() { if (update) { extraPropertiesStore().updateProperties(id, properties); } else { extraPropertiesStore().storeProperties(id, properties); } properties.clear(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy