/* * 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.federation; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import org.modeshape.common.logging.Logger; import org.modeshape.common.util.SecureHash.Algorithm; import org.modeshape.common.util.StringUtil; import org.modeshape.jcr.Connectors; import org.modeshape.jcr.JcrI18n; import org.modeshape.jcr.cache.MutableCachedNode; import org.modeshape.jcr.cache.NodeKey; import org.modeshape.jcr.cache.SessionCache; import org.modeshape.jcr.cache.document.DocumentStore; import org.modeshape.jcr.cache.document.DocumentTranslator; import org.modeshape.jcr.cache.document.LocalDocumentStore; import org.modeshape.jcr.cache.document.SessionNode; import org.modeshape.jcr.spi.federation.Connector; import org.modeshape.jcr.spi.federation.ConnectorException; import org.modeshape.jcr.spi.federation.DocumentChanges; import org.modeshape.jcr.spi.federation.DocumentReader; import org.modeshape.jcr.spi.federation.DocumentWriter; import org.modeshape.jcr.spi.federation.PageKey; import org.modeshape.jcr.spi.federation.Pageable; import org.modeshape.jcr.value.Name; import org.modeshape.jcr.value.Property; import org.modeshape.jcr.value.ReferenceFactory; import org.modeshape.jcr.value.basic.NodeKeyReference; import org.modeshape.jcr.value.basic.StringReference; import org.modeshape.jcr.value.binary.ExternalBinaryValue; import org.modeshape.schematic.SchematicEntry; import org.modeshape.schematic.document.Document; import org.modeshape.schematic.document.EditableDocument; /** * An implementation of {@link DocumentStore} which is used when federation is enabled * * @author Horia Chiorean (hchiorea@redhat.com) */ public class FederatedDocumentStore implements DocumentStore { private static final Logger LOGGER = Logger.getLogger(FederatedDocumentStore.class); private static final String FEDERATED_WORKSPACE_KEY = NodeKey.keyForWorkspaceName("federated_ws"); private final LocalDocumentStore localDocumentStore; private final Connectors connectors; private DocumentTranslator translator; /** * Creates a new instance with the given connectors and local store. * * @param connectors a {@code non-null} {@link Connectors} instance * @param localDocumentStore a {@code non-null} {@link LocalDocumentStore} instance */ public FederatedDocumentStore(Connectors connectors, LocalDocumentStore localDocumentStore) { this.connectors = connectors; this.localDocumentStore = localDocumentStore; } protected final DocumentTranslator translator() { if (translator == null) { translator = connectors.getDocumentTranslator(); } return translator; } @Override public LocalDocumentStore localStore() { return localDocumentStore; } @Override public String newDocumentKey( String parentKey, Name documentName, Name documentPrimaryType ) { if (isLocalSource(parentKey)) { return localStore().newDocumentKey(parentKey, documentName, null); } Connector connector = connectors.getConnectorForSourceKey(sourceKey(parentKey)); if (connector != null) { checkConnectorIsWritable(connector); String parentDocumentId = documentIdFromNodeKey(parentKey); String newChildId = connector.newDocumentId(parentDocumentId, documentName, documentPrimaryType); if (!StringUtil.isBlank(newChildId)) { return documentIdToNodeKey(connector.getSourceName(), newChildId).toString(); } } return null; } @Override public SchematicEntry storeIfAbsent(String key, Document document) { if (isLocalSource(key)) { return localStore().storeIfAbsent(key, document); } Connector connector = connectors.getConnectorForSourceKey(sourceKey(key)); if (connector != null) { checkConnectorIsWritable(connector); EditableDocument editableDocument = replaceNodeKeysWithDocumentIds(document); connector.storeDocument(editableDocument); } return null; } @Override public void updateDocument( String key, Document document, SessionNode sessionNode ) { if (isLocalSource(key)) { localStore().updateDocument(key, document, null); } else { Connector connector = connectors.getConnectorForSourceKey(sourceKey(key)); if (connector != null) { checkConnectorIsWritable(connector); EditableDocument editableDocument = replaceNodeKeysWithDocumentIds(document); String documentId = documentIdFromNodeKey(key); MutableCachedNode.NodeChanges nodeChanges = sessionNode.getNodeChanges(); DocumentChanges documentChanges = createDocumentChanges(nodeChanges, connector.getSourceName(), editableDocument, documentId); connector.updateDocument(documentChanges); } } } private DocumentChanges createDocumentChanges( MutableCachedNode.NodeChanges nodeChanges, String sourceName, EditableDocument editableDocument, String documentId ) { FederatedDocumentChanges documentChanges = new FederatedDocumentChanges(documentId, editableDocument); // property and mixin changes documentChanges.setPropertyChanges(nodeChanges.changedPropertyNames(), nodeChanges.removedPropertyNames()); documentChanges.setMixinChanges(nodeChanges.addedMixins(), nodeChanges.removedMixins()); // children LinkedHashMap<NodeKey, Name> appendedChildren = nodeChanges.appendedChildren(); validateSameSourceForAllNodes(sourceName, appendedChildren.keySet()); Map<NodeKey, LinkedHashMap<NodeKey, Name>> childrenInsertedBefore = nodeChanges.childrenInsertedBefore(); validateSameSourceForAllNodes(sourceName, childrenInsertedBefore.keySet()); Map<String, LinkedHashMap<String, Name>> processedChildrenInsertions = new HashMap<String, LinkedHashMap<String, Name>>( childrenInsertedBefore.size()); for (NodeKey childKey : childrenInsertedBefore.keySet()) { LinkedHashMap<NodeKey, Name> insertions = childrenInsertedBefore.get(childKey); validateSameSourceForAllNodes(sourceName, insertions.keySet()); processedChildrenInsertions.put(documentIdFromNodeKey(childKey), nodeKeyMapToIdentifierMap(insertions)); } documentChanges.setChildrenChanges(nodeKeyMapToIdentifierMap(appendedChildren), nodeKeyMapToIdentifierMap(nodeChanges.renamedChildren()), nodeKeySetToIdentifiersSet(nodeChanges.removedChildren()), processedChildrenInsertions); // parents Set<NodeKey> addedParents = nodeChanges.addedParents(); validateSameSourceForAllNodes(sourceName, addedParents); validateNodeKeyHasSource(sourceName, nodeChanges.newPrimaryParent()); documentChanges.setParentChanges(nodeKeySetToIdentifiersSet(addedParents), nodeKeySetToIdentifiersSet(nodeChanges.removedParents()), documentIdFromNodeKey(nodeChanges.newPrimaryParent())); // referrers Set<NodeKey> addedWeakReferrers = nodeChanges.addedWeakReferrers(); validateSameSourceForAllNodes(sourceName, addedWeakReferrers); Set<NodeKey> addedStrongReferrers = nodeChanges.addedStrongReferrers(); validateSameSourceForAllNodes(sourceName, addedStrongReferrers); documentChanges.setReferrerChanges(nodeKeySetToIdentifiersSet(addedWeakReferrers), nodeKeySetToIdentifiersSet(nodeChanges.removedWeakReferrers()), nodeKeySetToIdentifiersSet(addedStrongReferrers), nodeKeySetToIdentifiersSet(nodeChanges.removedStrongReferrers())); return documentChanges; } private void validateSameSourceForAllNodes( String sourceName, Collection<NodeKey> nodeKeys ) { for (NodeKey nodeKey : nodeKeys) { validateNodeKeyHasSource(sourceName, nodeKey); } } private void validateNodeKeyHasSource( String sourceName, NodeKey nodeKey ) { String sourceKey = NodeKey.keyForSourceName(sourceName); if (nodeKey != null && !sourceKey.equals(nodeKey.getSourceKey())) { throw new ConnectorException(JcrI18n.federationNodeKeyDoesNotBelongToSource, nodeKey, sourceName); } } private <T> Map<String, T> nodeKeyMapToIdentifierMap( Map<NodeKey, T> nodeKeysMap ) { Map<String, T> result = new HashMap<String, T>(nodeKeysMap.size()); for (NodeKey key : nodeKeysMap.keySet()) { result.put(documentIdFromNodeKey(key), nodeKeysMap.get(key)); } return result; } private <T> LinkedHashMap<String, T> nodeKeyMapToIdentifierMap( LinkedHashMap<NodeKey, T> nodeKeysMap ) { LinkedHashMap<String, T> result = new LinkedHashMap<String, T>(nodeKeysMap.size()); for (NodeKey key : nodeKeysMap.keySet()) { result.put(documentIdFromNodeKey(key), nodeKeysMap.get(key)); } return result; } private Set<String> nodeKeySetToIdentifiersSet( Set<NodeKey> nodeKeysSet ) { Set<String> result = new HashSet<String>(nodeKeysSet.size()); for (NodeKey key : nodeKeysSet) { result.add(documentIdFromNodeKey(key)); } return result; } @Override public SchematicEntry get( String key ) { if (isLocalSource(key)) { return localStore().get(key); } Connector connector = connectors.getConnectorForSourceKey(sourceKey(key)); if (connector != null) { Document document = null; if (connector instanceof Pageable && PageKey.isValidFormat(key)) { // a page was requested PageKey pageKey = new PageKey(key); String parentId = pageKey.getParentId(); pageKey = pageKey.withParentId(documentIdFromNodeKey(parentId)); document = ((Pageable)connector).getChildren(pageKey); } else { // interpret the key as a regular node id String docId = documentIdFromNodeKey(key); document = connector.getDocumentById(docId); } if (document != null) { // clone the document, so we don't alter the original EditableDocument editableDocument = replaceConnectorIdsWithNodeKeys(document, connector.getSourceName()); editableDocument = updateCaching(connector, editableDocument); editableDocument = updateQueryable(connector, editableDocument); return new ExternalSchematicEntry(key, editableDocument); } } return null; } @Override public List<SchematicEntry> load(Collection<String> keys) { Map<Boolean, List<String>> keysByLocality = keys.stream().collect(Collectors.groupingBy(this::isLocalSource)); List<String> localKeys = keysByLocality.get(Boolean.TRUE); List<SchematicEntry> docsByKey = localKeys == null ? new ArrayList<>() : localStore().load(localKeys); List<String> externalKeys = keysByLocality.get(Boolean.FALSE); if (externalKeys != null) { docsByKey.addAll(externalKeys.stream() .map(this::get) .filter(Objects::nonNull) .collect(Collectors.toList())); } return docsByKey; } private EditableDocument updateCaching(Connector connector, EditableDocument editableDocument) { DocumentReader reader = new FederatedDocumentReader(translator(), editableDocument); if (!reader.isCacheable() || !connector.isCacheable()) { translator.setCacheable(editableDocument, false); return editableDocument; } return editableDocument; } private EditableDocument updateQueryable( Connector connector, EditableDocument editableDocument ) { if (!connector.isQueryable()) { translator.setQueryable(editableDocument, false); return editableDocument; } return editableDocument; } @Override public boolean containsKey( String key ) { if (isLocalSource(key)) { return localStore().containsKey(key); } Connector connector = connectors.getConnectorForSourceKey(sourceKey(key)); return connector != null && connector.hasDocument(documentIdFromNodeKey(key)); } @Override public boolean remove( String key ) { if (isLocalSource(key)) { boolean result = localStore().remove(key); connectors.internalNodeRemoved(key); return result; } Connector connector = connectors.getConnectorForSourceKey(sourceKey(key)); if (connector != null) { checkConnectorIsWritable(connector); boolean result = connector.removeDocument(documentIdFromNodeKey(key)); connectors.externalNodeRemoved(key); return result; } return false; } @Override public boolean lockDocuments( Collection<String> keys ) { return localDocumentStore.lockDocuments(keys); } @Override public boolean lockDocuments(String... keys) { return localDocumentStore.lockDocuments(keys); } @Override public EditableDocument edit( String key, boolean createIfMissing ) { if (isLocalSource(key)) { return localStore().edit(key, createIfMissing); } // It's federated, so we have to use the federated logic ... SchematicEntry entry = get(key); return entry != null ? entry.source().editable() : null; } @Override public void setLocalSourceKey( String localSourceKey ) { this.localDocumentStore.setLocalSourceKey(localSourceKey); } @Override public String getLocalSourceKey() { return localDocumentStore.getLocalSourceKey(); } @Override public String createExternalProjection( String projectedNodeKey, String sourceName, String externalPath, String alias, SessionCache systemSession) { String sourceKey = NodeKey.keyForSourceName(sourceName); Connector connector = connectors.getConnectorForSourceKey(sourceKey); if (connector != null) { String externalNodeId = connector.getDocumentId(externalPath); if (externalNodeId != null) { String externalNodeKey = documentIdToNodeKeyString(sourceName, externalNodeId); connectors.addProjection(externalNodeKey, projectedNodeKey, alias, systemSession); return externalNodeKey; } } return null; } @Override public Document getChildrenBlock( String key ) { if (isLocalSource(key)) { return localStore().getChildrenBlock(key); } Connector connector = connectors.getConnectorForSourceKey(sourceKey(key)); if (connector != null && connector instanceof Pageable) { key = documentIdFromNodeKey(key); PageKey blockKey = new PageKey(key); Document childrenBlock = ((Pageable)connector).getChildren(blockKey); if (childrenBlock != null) { return replaceConnectorIdsWithNodeKeys(childrenBlock, connector.getSourceName()); } } return null; } @Override public Document getChildReference( String parentKey, String childKey ) { if (isLocalSource(parentKey)) { return localStore().getChildReference(parentKey, childKey); } Connector connector = connectors.getConnectorForSourceKey(sourceKey(parentKey)); if (connector != null) { parentKey = documentIdFromNodeKey(parentKey); childKey = documentIdFromNodeKey(childKey); Document doc = connector.getChildReference(parentKey, childKey); if (doc != null) { String key = doc.getString(DocumentTranslator.KEY); key = documentIdToNodeKeyString(connector.getSourceName(), key); doc = doc.with(DocumentTranslator.KEY, key); } return doc; } return null; } @Override public ExternalBinaryValue getExternalBinary( String sourceName, String id ) { Connector connector = connectors.getConnectorForSourceName(sourceName); if (connector == null) { LOGGER.debug("Connector not found for source name {0} while trying to get a binary value", sourceName); return null; } return connector.getBinaryValue(id); } private boolean isLocalSource( String key ) { String localSourceKey = getLocalSourceKey(); return !NodeKey.isValidFormat(key) // the key isn't a std key format (probably some internal format) || StringUtil.isBlank(localSourceKey) // there isn't a local source configured yet (e.g. system startup) || key.startsWith(localSourceKey) // the sources differ || isBinaryMetadataDocumentKey(key); // it's the metadata document with reference counts for a SHA1 binary value } private boolean isBinaryMetadataDocumentKey( String key ) { return key.endsWith("-ref") && Algorithm.SHA_1.isHexadecimal(key.substring(0, 40)); // 40 hexadecimal characters long } private String sourceKey( String nodeKey ) { return NodeKey.sourceKey(nodeKey); } private String documentIdToNodeKeyString( String sourceName, String documentId ) { return documentIdToNodeKey(sourceName, documentId).toString(); } static NodeKey documentIdToNodeKey( String sourceName, String documentId ) { String sourceKey = NodeKey.keyForSourceName(sourceName); return new NodeKey(sourceKey, FEDERATED_WORKSPACE_KEY, documentId); } private String documentIdFromNodeKey( String nodeKey ) { return new NodeKey(nodeKey).getIdentifier(); } private String documentIdFromNodeKey( NodeKey nodeKey ) { return nodeKey != null ? nodeKey.getIdentifier() : null; } private EditableDocument replaceConnectorIdsWithNodeKeys( Document externalDocument, String sourceName ) { DocumentReader reader = new FederatedDocumentReader(translator(), externalDocument); DocumentWriter writer = new FederatedDocumentWriter(translator(), externalDocument); // replace document id with node key String externalDocumentId = reader.getDocumentId(); String externalDocumentKey = null; if (!StringUtil.isBlank(externalDocumentId)) { externalDocumentKey = documentIdToNodeKeyString(sourceName, externalDocumentId); writer.setId(externalDocumentKey); } // replace the id of each parent and add the optional federated parent List<String> parentKeys = new ArrayList<String>(); for (String parentId : reader.getParentIds()) { String parentKey = documentIdToNodeKeyString(sourceName, parentId); parentKeys.add(parentKey); } // replace the id of each block (if they exist) EditableDocument childrenInfo = writer.document().getDocument(DocumentTranslator.CHILDREN_INFO); if (childrenInfo != null) { String nextBlockKey = childrenInfo.getString(DocumentTranslator.NEXT_BLOCK); if (!StringUtil.isBlank(nextBlockKey)) { childrenInfo.setString(DocumentTranslator.NEXT_BLOCK, documentIdToNodeKeyString(sourceName, nextBlockKey)); } String lastBlockKey = childrenInfo.getString(DocumentTranslator.LAST_BLOCK); if (!StringUtil.isBlank(lastBlockKey)) { childrenInfo.setString(DocumentTranslator.LAST_BLOCK, documentIdToNodeKeyString(sourceName, lastBlockKey)); } } // create the federated node key - external project back reference if (externalDocumentKey != null) { String projectedNodeKey = connectors.getProjectedNodeKey(externalDocumentKey); if (!StringUtil.isBlank(projectedNodeKey)) { parentKeys.add(projectedNodeKey); } } writer.setParents(parentKeys); // process each child in the same way List<Document> updatedChildren = new ArrayList<Document>(); for (Document child : reader.getChildren()) { EditableDocument childWithReplacedIds = replaceConnectorIdsWithNodeKeys(child, sourceName); updatedChildren.add(childWithReplacedIds); } writer.setChildren(updatedChildren); // process the properties to look for **INTERNAL** references ... for (Property property : reader.getProperties().values()) { if (property.isEmpty()) continue; if (property.isReference()) { if (property.isSingle()) { Object value = convertReferenceValue(property.getFirstValue(), sourceName); writer.addProperty(property.getName(), value); } else { assert property.isMultiple(); Object[] values = property.getValuesAsArray(); for (int i = 0; i != values.length; ++i) { values[i] = convertReferenceValue(values[i], sourceName); } writer.addProperty(property.getName(), values); } } } return writer.document(); } private Object convertReferenceValue( Object value, String sourceName ) { String localSourceKey = getLocalSourceKey(); if (value instanceof NodeKeyReference) { NodeKeyReference ref = (NodeKeyReference)value; NodeKey key = ref.getNodeKey(); NodeKey converted = documentIdToNodeKey(sourceName, key.toString()); boolean foreign = !converted.getSourceKey().equals(localSourceKey); ReferenceFactory factory = ref.isWeak() ? translator.getReferenceFactory() : translator.getReferenceFactory(); return factory.create(converted, foreign); } else if (value instanceof StringReference) { StringReference ref = (StringReference)value; NodeKey converted = documentIdToNodeKey(sourceName, ref.toString()); boolean foreign = !converted.getSourceKey().equals(localSourceKey); ReferenceFactory factory = ref.isWeak() ? translator.getReferenceFactory() : translator.getReferenceFactory(); return factory.create(converted, foreign); } return value; } private EditableDocument replaceNodeKeysWithDocumentIds( Document document ) { DocumentReader reader = new FederatedDocumentReader(translator(), document); DocumentWriter writer = new FederatedDocumentWriter(translator(), document); // replace node key with document id String documentNodeKey = reader.getDocumentId(); assert documentNodeKey != null; String externalDocumentId = documentIdFromNodeKey(documentNodeKey); writer.setId(externalDocumentId); // replace the node key with the id of each parent and remove the optional federated parent List<String> parentKeys = reader.getParentIds(); String projectedNodeKey = connectors.getProjectedNodeKey(documentNodeKey); if (!StringUtil.isBlank(projectedNodeKey)) { parentKeys.remove(projectedNodeKey); } List<String> parentIds = new ArrayList<String>(); for (String parentKey : parentKeys) { String parentId = documentIdFromNodeKey(parentKey); parentIds.add(parentId); } writer.setParents(parentIds); // process each child in the same way List<Document> updatedChildren = new ArrayList<Document>(); for (Document child : reader.getChildren()) { EditableDocument childWithReplacedIds = replaceNodeKeysWithDocumentIds(child); updatedChildren.add(childWithReplacedIds); } writer.setChildren(updatedChildren); return writer.document(); } private void checkConnectorIsWritable( Connector connector ) throws ConnectorException { if (connector.isReadonly()) { throw new ConnectorException(JcrI18n.connectorIsReadOnly.text(connector.getSourceName())); } } private static class ExternalSchematicEntry implements SchematicEntry { private final EditableDocument result; private final String id; protected ExternalSchematicEntry(String id, EditableDocument result) { this.result = result; this.id = id; } @Override public Document source() { return result; } @Override public Document content() { return result; } @Override public Document getMetadata() { throw new UnsupportedOperationException("Not supported"); } @Override public String id() { return id; } } }