blob: 7279ef7b66f7df206f2a10b285cd296983711fe8 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.jackrabbit.oak.jcr.xml;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import java.util.UUID;
import javax.jcr.ImportUUIDBehavior;
import javax.jcr.ItemExistsException;
import javax.jcr.PathNotFoundException;
import javax.jcr.PropertyType;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.lock.LockException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.NodeDefinition;
import javax.jcr.nodetype.PropertyDefinition;
import javax.jcr.version.VersionException;
import javax.jcr.version.VersionManager;
import com.google.common.base.Function;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.oak.api.ContentSession;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.jcr.delegate.SessionDelegate;
import org.apache.jackrabbit.oak.jcr.security.AccessManager;
import org.apache.jackrabbit.oak.jcr.session.SessionContext;
import org.apache.jackrabbit.oak.jcr.session.WorkspaceImpl;
import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager;
import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
import org.apache.jackrabbit.oak.plugins.tree.TreeUtil;
import org.apache.jackrabbit.oak.spi.nodetype.DefinitionProvider;
import org.apache.jackrabbit.oak.spi.nodetype.EffectiveNodeType;
import org.apache.jackrabbit.oak.spi.nodetype.EffectiveNodeTypeProvider;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.Permissions;
import org.apache.jackrabbit.oak.spi.xml.Importer;
import org.apache.jackrabbit.oak.spi.xml.NodeInfo;
import org.apache.jackrabbit.oak.spi.xml.PropInfo;
import org.apache.jackrabbit.oak.spi.xml.ProtectedItemImporter;
import org.apache.jackrabbit.oak.spi.xml.ProtectedNodeImporter;
import org.apache.jackrabbit.oak.spi.xml.ProtectedPropertyImporter;
import org.apache.jackrabbit.oak.spi.xml.ReferenceChangeTracker;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.NODE_TYPES_PATH;
public class ImporterImpl implements Importer {
private static final Logger log = LoggerFactory.getLogger(ImporterImpl.class);
private final Tree importTargetTree;
private final Tree ntTypesRoot;
private final int uuidBehavior;
private final String userID;
private final AccessManager accessManager;
private final EffectiveNodeTypeProvider effectiveNodeTypeProvider;
private final DefinitionProvider definitionProvider;
private final IdResolver idLookup;
private final Stack<Tree> parents;
/**
* helper object that keeps track of remapped uuid's and imported reference
* properties that might need correcting depending on the uuid mappings
*/
private final ReferenceChangeTracker refTracker;
private final List<ProtectedItemImporter> pItemImporters = new ArrayList<ProtectedItemImporter>();
/**
* Currently active importer for protected nodes.
*/
private ProtectedNodeImporter pnImporter;
/**
* Creates a new importer instance.
*
* @param absPath The absolute JCR paths such as passed to the JCR call.
* @param sessionContext The context of the editing session
* @param root The write {@code Root}, which in case of a workspace import
* is different from the {@code Root} associated with the editing session.
* @param uuidBehavior The uuid behavior
* @param isWorkspaceImport {@code true} if this is a workspace import,
* {@code false} otherwise.
* @throws javax.jcr.RepositoryException If the initial validation of the
* path or the state of target node/session fails.
*/
public ImporterImpl(String absPath,
SessionContext sessionContext,
Root root,
int uuidBehavior,
boolean isWorkspaceImport) throws RepositoryException {
String oakPath = sessionContext.getOakPath(absPath);
if (oakPath == null) {
throw new RepositoryException("Invalid name or path: " + absPath);
}
if (!PathUtils.isAbsolute(oakPath)) {
throw new RepositoryException("Not an absolute path: " + absPath);
}
SessionDelegate sd = sessionContext.getSessionDelegate();
if (isWorkspaceImport && sd.hasPendingChanges()) {
throw new RepositoryException("Pending changes on session. Cannot run workspace import.");
}
this.uuidBehavior = uuidBehavior;
userID = sd.getAuthInfo().getUserID();
importTargetTree = root.getTree(oakPath);
if (!importTargetTree.exists()) {
throw new PathNotFoundException(absPath);
}
WorkspaceImpl wsp = sessionContext.getWorkspace();
VersionManager vMgr = wsp.getVersionManager();
if (!vMgr.isCheckedOut(absPath)) {
throw new VersionException("Target node is checked in.");
}
boolean hasLocking = sessionContext.getRepository().getDescriptorValue(Repository.OPTION_LOCKING_SUPPORTED).getBoolean();
if (importTargetTree.getStatus() != Tree.Status.NEW && hasLocking && wsp.getLockManager().isLocked(absPath)) {
throw new LockException("Target node is locked.");
}
effectiveNodeTypeProvider = wsp.getNodeTypeManager();
definitionProvider = wsp.getNodeTypeManager();
ntTypesRoot = root.getTree(NODE_TYPES_PATH);
accessManager = sessionContext.getAccessManager();
idLookup = new IdResolver(root, sd.getContentSession());
refTracker = new ReferenceChangeTracker();
parents = new Stack<Tree>();
parents.push(importTargetTree);
pItemImporters.clear();
for (ProtectedItemImporter importer : sessionContext.getProtectedItemImporters()) {
// FIXME this passes the session scoped name path mapper also for workspace imports
if (importer.init(sessionContext.getSession(), root, sessionContext, isWorkspaceImport, uuidBehavior, refTracker, sessionContext.getSecurityProvider())) {
pItemImporters.add(importer);
}
}
}
private Tree createTree(@NotNull Tree parent, @NotNull NodeInfo nInfo, @Nullable String uuid) throws RepositoryException {
String ntName = nInfo.getPrimaryTypeName();
Tree child = TreeUtil.addChild(
parent, nInfo.getName(), ntName, ntTypesRoot, userID);
if (ntName != null) {
accessManager.checkPermissions(child, child.getProperty(JcrConstants.JCR_PRIMARYTYPE), Permissions.NODE_TYPE_MANAGEMENT);
}
if (uuid != null) {
child.setProperty(JcrConstants.JCR_UUID, uuid);
}
for (String mixin : nInfo.getMixinTypeNames()) {
TreeUtil.addMixin(child, mixin, ntTypesRoot, userID);
}
return child;
}
private void createProperty(Tree tree, PropInfo pInfo, PropertyDefinition def) throws RepositoryException {
tree.setProperty(pInfo.asPropertyState(def));
int type = pInfo.getType();
if (type == PropertyType.REFERENCE || type == PropertyType.WEAKREFERENCE) {
// store reference for later resolution
refTracker.processedReference(new Reference(tree, pInfo.getName()));
}
}
private Tree resolveUUIDConflict(Tree parent,
Tree conflicting,
String conflictingId,
NodeInfo nodeInfo) throws RepositoryException {
Tree tree;
if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW) {
// create new with new uuid
tree = createTree(parent, nodeInfo, UUID.randomUUID().toString());
// remember uuid mapping
if (isNodeType(tree, JcrConstants.MIX_REFERENCEABLE)) {
refTracker.put(nodeInfo.getUUID(), TreeUtil.getString(tree, JcrConstants.JCR_UUID));
}
} else if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW) {
// if conflicting node is shareable, then clone it
String msg = "a node with uuid " + nodeInfo.getUUID() + " already exists!";
log.debug(msg);
throw new ItemExistsException(msg);
} else if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING) {
if (conflicting == null) {
// since the conflicting node can't be read,
// we can't remove it
String msg = "node with uuid " + conflictingId + " cannot be removed";
log.debug(msg);
throw new RepositoryException(msg);
}
// make sure conflicting node is not importTargetNode or an ancestor thereof
if (importTargetTree.getPath().startsWith(conflicting.getPath())) {
String msg = "cannot remove ancestor node";
log.debug(msg);
throw new ConstraintViolationException(msg);
}
// remove conflicting
conflicting.remove();
// create new with given uuid
tree = createTree(parent, nodeInfo, nodeInfo.getUUID());
} else if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING) {
if (conflicting == null) {
// since the conflicting node can't be read,
// we can't replace it
String msg = "node with uuid " + conflictingId + " cannot be replaced";
log.debug(msg);
throw new RepositoryException(msg);
}
if (conflicting.isRoot()) {
String msg = "root node cannot be replaced";
log.debug(msg);
throw new RepositoryException(msg);
}
// 'replace' current parent with parent of conflicting
parent = conflicting.getParent();
// replace child node
//TODO ordering! (what happened to replace?)
conflicting.remove();
tree = createTree(parent, nodeInfo, nodeInfo.getUUID());
} else {
String msg = "unknown uuidBehavior: " + uuidBehavior;
log.debug(msg);
throw new RepositoryException(msg);
}
return tree;
}
private void importProperties(@NotNull Tree tree,
@NotNull List<PropInfo> propInfos,
boolean ignoreRegular) throws RepositoryException {
// process properties
for (PropInfo pi : propInfos) {
// find applicable definition
//TODO find better heuristics?
EffectiveNodeType ent = effectiveNodeTypeProvider.getEffectiveNodeType(tree);
PropertyDefinition def = ent.getPropertyDefinition(pi.getName(), pi.getType(), pi.isUnknownMultiple());
if (def.isProtected()) {
// skip protected property
log.debug("Protected property {}", pi.getName());
// notify the ProtectedPropertyImporter.
for (ProtectedPropertyImporter ppi : getPropertyImporters()) {
if (ppi.handlePropInfo(tree, pi, def)) {
log.debug("Protected property -> delegated to ProtectedPropertyImporter");
break;
} /* else: p-i-Importer isn't able to deal with this property. try next pp-importer */
}
} else if (!ignoreRegular) {
// regular property -> create the property
createProperty(tree, pi, def);
}
}
for (ProtectedPropertyImporter ppi : getPropertyImporters()) {
ppi.propertiesCompleted(tree);
}
}
private Iterable<ProtectedPropertyImporter> getPropertyImporters() {
return Iterables.filter(Iterables.transform(pItemImporters, new Function<ProtectedItemImporter, ProtectedPropertyImporter>() {
@Nullable
@Override
public ProtectedPropertyImporter apply(@Nullable ProtectedItemImporter importer) {
if (importer instanceof ProtectedPropertyImporter) {
return (ProtectedPropertyImporter) importer;
} else {
return null;
}
}
}), Predicates.notNull());
}
private Iterable<ProtectedNodeImporter> getNodeImporters() {
return Iterables.filter(Iterables.transform(pItemImporters, new Function<ProtectedItemImporter, ProtectedNodeImporter>() {
@Nullable
@Override
public ProtectedNodeImporter apply(@Nullable ProtectedItemImporter importer) {
if (importer instanceof ProtectedNodeImporter) {
return (ProtectedNodeImporter) importer;
} else {
return null;
}
}
}), Predicates.notNull());
}
//-----------------------------------------------------------< Importer >---
@Override
public void start() throws RepositoryException {
// nop
}
@Override
public void startNode(NodeInfo nodeInfo, List<PropInfo> propInfos)
throws RepositoryException {
Tree parent = parents.peek();
Tree tree = null;
String id = nodeInfo.getUUID();
String nodeName = nodeInfo.getName();
String ntName = nodeInfo.getPrimaryTypeName();
if (parent == null) {
log.debug("Skipping node: {}", nodeName);
// parent node was skipped, skip this child node too
parents.push(null); // push null onto stack for skipped node
// notify the p-i-importer
if (pnImporter != null) {
pnImporter.startChildInfo(nodeInfo, propInfos);
}
return;
}
NodeDefinition parentDef = getDefinition(parent);
if (parentDef.isProtected()) {
// skip protected node
parents.push(null);
log.debug("Skipping protected node: {}", nodeName);
if (pnImporter != null) {
// pnImporter was already started (current nodeInfo is a sibling)
// notify it about this child node.
pnImporter.startChildInfo(nodeInfo, propInfos);
} else {
// no importer defined yet:
// test if there is a ProtectedNodeImporter among the configured
// importers that can handle this.
// if there is one, notify the ProtectedNodeImporter about the
// start of a item tree that is protected by this parent. If it
// potentially is able to deal with it, notify it about the child node.
for (ProtectedNodeImporter pni : getNodeImporters()) {
if (pni.start(parent)) {
log.debug("Protected node -> delegated to ProtectedNodeImporter");
pnImporter = pni;
pnImporter.startChildInfo(nodeInfo, propInfos);
break;
} /* else: p-i-Importer isn't able to deal with the protected tree.
try next. and if none can handle the passed parent the
tree below will be skipped */
}
}
return;
}
if (parent.hasChild(nodeName)) {
// a node with that name already exists...
Tree existing = parent.getChild(nodeName);
NodeDefinition def = getDefinition(existing);
if (!def.allowsSameNameSiblings()) {
// existing doesn't allow same-name siblings,
// check for potential conflicts
if (def.isProtected() && isNodeType(existing, ntName)) {
/*
use the existing node as parent for the possible subsequent
import of a protected tree, that the protected node importer
may or may not be able to deal with.
-> upon the next 'startNode' the check for the parent being
protected will notify the protected node importer.
-> if the importer is able to deal with that node it needs
to care of the complete subtree until it is notified
during the 'endNode' call.
-> if the import can't deal with that node or if that node
is the a leaf in the tree to be imported 'end' will
not have an effect on the importer, that was never started.
*/
log.debug("Skipping protected node: {}", existing);
parents.push(existing);
/**
* let ProtectedPropertyImporters handle the properties
* associated with the imported node. this may include overwriting,
* merging or just adding missing properties.
*/
importProperties(existing, propInfos, true);
return;
}
if (def.isAutoCreated() && isNodeType(existing, ntName)) {
// this node has already been auto-created, no need to create it
tree = existing;
} else {
// edge case: colliding node does have same uuid
// (see http://issues.apache.org/jira/browse/JCR-1128)
String existingIdentifier = IdentifierManager.getIdentifier(existing);
if (!(existingIdentifier.equals(id)
&& (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING
|| uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING))) {
throw new ItemExistsException(
"Node with the same UUID exists:" + existing);
}
// fall through
}
}
}
if (tree == null) {
// create node
if (id == null) {
// no potential uuid conflict, always add new node
tree = createTree(parent, nodeInfo, null);
} else if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW) {
// always create a new UUID even if no
// conflicting node exists. see OAK-1244
tree = createTree(parent, nodeInfo, UUID.randomUUID().toString());
// remember uuid mapping
if (isNodeType(tree, JcrConstants.MIX_REFERENCEABLE)) {
refTracker.put(nodeInfo.getUUID(), TreeUtil.getString(tree, JcrConstants.JCR_UUID));
}
} else {
Tree conflicting = idLookup.getConflictingTree(id);
if (conflicting != null && conflicting.exists()) {
// resolve uuid conflict
tree = resolveUUIDConflict(parent, conflicting, id, nodeInfo);
if (tree == null) {
// no new node has been created, so skip this node
parents.push(null); // push null onto stack for skipped node
log.debug("Skipping existing node {}", nodeInfo.getName());
return;
}
} else {
// create new with given uuid
tree = createTree(parent, nodeInfo, id);
}
}
}
// process properties
importProperties(tree, propInfos, false);
if (tree.exists()) {
parents.push(tree);
}
}
@Override
public void endNode(NodeInfo nodeInfo) throws RepositoryException {
Tree parent = parents.pop();
if (parent == null) {
if (pnImporter != null) {
pnImporter.endChildInfo();
}
} else if (getDefinition(parent).isProtected()) {
if (pnImporter != null) {
pnImporter.end(parent);
// and reset the pnImporter field waiting for the next protected
// parent -> selecting again from available importers
pnImporter = null;
}
}
idLookup.rememberImportedUUIDs(parent);
}
@Override
public void end() throws RepositoryException {
/**
* adjust references that refer to uuids which have been mapped to
* newly generated uuids on import
*/
// 1. let protected property/node importers handle protected ref-properties
// and (protected) properties underneath a protected parent node.
for (ProtectedItemImporter ppi : pItemImporters) {
ppi.processReferences();
}
// 2. regular non-protected properties.
Iterator<Object> iter = refTracker.getProcessedReferences();
while (iter.hasNext()) {
Object ref = iter.next();
if (!(ref instanceof Reference)) {
continue;
}
Reference reference = (Reference) ref;
if (reference.isMultiple()) {
Iterable<String> values = reference.property.getValue(Type.STRINGS);
List<String> newValues = Lists.newArrayList();
for (String original : values) {
String adjusted = refTracker.get(original);
if (adjusted != null) {
newValues.add(adjusted);
} else {
// reference doesn't need adjusting, just copy old value
newValues.add(original);
}
}
reference.setProperty(newValues);
} else {
String original = reference.property.getValue(Type.STRING);
String adjusted = refTracker.get(original);
if (adjusted != null) {
reference.setProperty(adjusted);
}
}
}
refTracker.clear();
}
private boolean isNodeType(Tree tree, String ntName) throws RepositoryException {
return effectiveNodeTypeProvider.isNodeType(tree, ntName);
}
private NodeDefinition getDefinition(Tree tree) throws RepositoryException {
if (tree.isRoot()) {
return definitionProvider.getRootDefinition();
} else {
return definitionProvider.getDefinition(tree.getParent(), tree);
}
}
private static final class Reference {
private final Tree tree;
private final PropertyState property;
private Reference(Tree tree, String propertyName) {
this.tree = tree;
this.property = tree.getProperty(propertyName);
}
private boolean isMultiple() {
return property.isArray();
}
private void setProperty(String newValue) {
PropertyState prop = PropertyStates.createProperty(property.getName(), newValue, property.getType().tag());
tree.setProperty(prop);
}
private void setProperty(Iterable<String> newValues) {
PropertyState prop = PropertyStates.createProperty(property.getName(), newValues, property.getType());
tree.setProperty(prop);
}
}
/**
* Resolves 'uuid' property values to {@code Tree} objects and optionally
* keeps track of newly imported UUIDs.
*/
private static final class IdResolver {
/**
* There are two IdentifierManagers used.
*
* 1) currentStateIdManager - Associated with current root on which all import
* operations are being performed
*
* 2) baseStateIdManager - Associated with the initial root on which
* no modifications are performed
*/
private final IdentifierManager currentStateIdManager;
private final IdentifierManager baseStateIdManager;
/**
* Set of newly created uuid from nodes which are
* created in this import, which are only remembered if the editing
* session doesn't have any pending transient changes preventing this
* performance optimisation from working properly (see OAK-2246).
*/
private final Set<String> importedUUIDs;
private IdResolver(@NotNull Root root, @NotNull ContentSession contentSession) {
currentStateIdManager = new IdentifierManager(root);
baseStateIdManager = new IdentifierManager(contentSession.getLatestRoot());
if (!root.hasPendingChanges()) {
importedUUIDs = new HashSet<String>();
} else {
importedUUIDs = null;
}
}
@Nullable
private Tree getConflictingTree(@NotNull String id) {
//1. First check from base state that tree corresponding to
//this id exist
Tree conflicting = baseStateIdManager.getTree(id);
if (conflicting == null && importedUUIDs != null) {
//1.a. Check if id is found in newly created nodes
if (importedUUIDs.contains(id)) {
conflicting = currentStateIdManager.getTree(id);
}
} else {
//1.b Re obtain the conflicting tree from Id Manager
//associated with current root. Such that any operation
//on it gets reflected in later operations
//In case a tree with same id was removed earlier then it
//would return null
conflicting = currentStateIdManager.getTree(id);
}
return conflicting;
}
private void rememberImportedUUIDs(@Nullable Tree tree) {
if (tree == null || importedUUIDs == null) {
return;
}
String uuid = TreeUtil.getString(tree, JcrConstants.JCR_UUID);
if (uuid != null) {
importedUUIDs.add(uuid);
}
for (Tree child : tree.getChildren()) {
rememberImportedUUIDs(child);
}
}
}
}