/*
* 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.core.xml;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import java.util.ArrayList;
import javax.jcr.AccessDeniedException;
import javax.jcr.ImportUUIDBehavior;
import javax.jcr.ItemExistsException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Property;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.ValueFormatException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.NodeDefinition;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.config.ImportConfig;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.security.authorization.Permission;
import org.apache.jackrabbit.core.util.ReferenceChangeTracker;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.QPropertyDefinition;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <code>SessionImporter</code> ...
*/
public class SessionImporter implements Importer {
private static Logger log = LoggerFactory.getLogger(SessionImporter.class);
private final SessionImpl session;
private final NodeImpl importTargetNode;
private final int uuidBehavior;
private Stack<NodeImpl> 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();
/**
* Currently active importer for protected nodes.
*/
private ProtectedNodeImporter pnImporter = null;
/**
* Creates a new <code>SessionImporter</code> instance.
*
* @param importTargetNode the target node
* @param session session
* @param uuidBehavior any of the constants declared by
* {@link javax.jcr.ImportUUIDBehavior}
*/
public SessionImporter(NodeImpl importTargetNode,
SessionImpl session,
int uuidBehavior) {
this(importTargetNode, session, uuidBehavior, null);
}
/**
* Creates a new <code>SessionImporter</code> instance.
*
* @param importTargetNode the target node
* @param session session
* @param uuidBehavior the desired uuid behavior as defined
* by {@link javax.jcr.ImportUUIDBehavior}.
* @param config
*/
public SessionImporter(NodeImpl importTargetNode, SessionImpl session,
int uuidBehavior, ImportConfig config) {
this.importTargetNode = importTargetNode;
this.session = session;
this.uuidBehavior = uuidBehavior;
refTracker = new ReferenceChangeTracker();
parents = new Stack<NodeImpl>();
parents.push(importTargetNode);
if (config != null) {
List<ProtectedItemImporter> iList = (List<ProtectedItemImporter>) config.getProtectedItemImporters();
for (ProtectedItemImporter importer : iList) {
if (importer.init(session, session, false, uuidBehavior, refTracker)) {
pItemImporters.add(importer);
}
}
}
// missing config -> initialize defaults.
if (pItemImporters.isEmpty()) {
ProtectedItemImporter def = new DefaultProtectedItemImporter();
if (def.init(session, session, false, uuidBehavior, refTracker)) {
pItemImporters.add(def);
}
}
}
/**
* make sure the editing session is allowed create nodes with a
* specified node type (and ev. mixins),<br>
* NOTE: this check is not executed in a single place as the parent
* may change in case of
* {@link javax.jcr.ImportUUIDBehavior#IMPORT_UUID_COLLISION_REPLACE_EXISTING IMPORT_UUID_COLLISION_REPLACE_EXISTING}.
*
* @param parent parent node
* @param nodeName the name
* @throws RepositoryException if an error occurs
*/
protected void checkPermission(NodeImpl parent, Name nodeName)
throws RepositoryException {
if (!session.getAccessManager().isGranted(session.getQPath(parent.getPath()), nodeName, Permission.NODE_TYPE_MNGMT)) {
throw new AccessDeniedException("Insufficient permission.");
}
}
protected NodeImpl createNode(NodeImpl parent,
Name nodeName,
Name nodeTypeName,
Name[] mixinNames,
NodeId id)
throws RepositoryException {
NodeImpl node;
// add node
node = parent.addNode(nodeName, nodeTypeName, id);
// add mixins
if (mixinNames != null) {
for (Name mixinName : mixinNames) {
node.addMixin(mixinName);
}
}
return node;
}
protected void createProperty(NodeImpl node, PropInfo pInfo, QPropertyDefinition def) throws RepositoryException {
// convert serialized values to Value objects
Value[] va = pInfo.getValues(pInfo.getTargetType(def), session);
// multi- or single-valued property?
Name name = pInfo.getName();
int type = pInfo.getType();
if (va.length == 1 && !def.isMultiple()) {
Exception e = null;
try {
// set single-value
node.setProperty(name, va[0]);
} catch (ValueFormatException vfe) {
e = vfe;
} catch (ConstraintViolationException cve) {
e = cve;
}
if (e != null) {
// setting single-value failed, try setting value array
// as a last resort (in case there are ambiguous property
// definitions)
node.setProperty(name, va, type);
}
} else {
// can only be multi-valued (n == 0 || n > 1)
node.setProperty(name, va, type);
}
if (type == PropertyType.REFERENCE || type == PropertyType.WEAKREFERENCE) {
// store reference for later resolution
refTracker.processedReference(node.getProperty(name));
}
}
protected NodeImpl resolveUUIDConflict(NodeImpl parent,
NodeImpl conflicting,
NodeInfo nodeInfo)
throws RepositoryException {
NodeImpl node;
if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW) {
// create new with new uuid
checkPermission(parent, nodeInfo.getName());
node = createNode(parent, nodeInfo.getName(),
nodeInfo.getNodeTypeName(), nodeInfo.getMixinNames(), null);
// remember uuid mapping
if (node.isNodeType(NameConstants.MIX_REFERENCEABLE)) {
refTracker.mappedId(nodeInfo.getId(), node.getNodeId());
}
} else if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW) {
// if conflicting node is shareable, then clone it
if (conflicting.isNodeType(NameConstants.MIX_SHAREABLE)) {
parent.clone(conflicting, nodeInfo.getName());
return null;
}
String msg = "a node with uuid " + nodeInfo.getId() + " already exists!";
log.debug(msg);
throw new ItemExistsException(msg);
} else if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING) {
// make sure conflicting node is not importTargetNode or an ancestor thereof
if (importTargetNode.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
checkPermission(parent, nodeInfo.getName());
node = createNode(parent, nodeInfo.getName(),
nodeInfo.getNodeTypeName(), nodeInfo.getMixinNames(),
nodeInfo.getId());
} else if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING) {
if (conflicting.getDepth() == 0) {
String msg = "root node cannot be replaced";
log.debug(msg);
throw new RepositoryException(msg);
}
// 'replace' current parent with parent of conflicting
parent = (NodeImpl) conflicting.getParent();
// replace child node
checkPermission(parent, nodeInfo.getName());
node = parent.replaceChildNode(nodeInfo.getId(), nodeInfo.getName(),
nodeInfo.getNodeTypeName(), nodeInfo.getMixinNames());
} else {
String msg = "unknown uuidBehavior: " + uuidBehavior;
log.debug(msg);
throw new RepositoryException(msg);
}
return node;
}
//-------------------------------------------------------------< Importer >
/**
* {@inheritDoc}
*/
public void start() throws RepositoryException {
// nop
}
/**
* {@inheritDoc}
*/
public void startNode(NodeInfo nodeInfo, List<PropInfo> propInfos)
throws RepositoryException {
NodeImpl parent = parents.peek();
// process node
NodeImpl node = null;
NodeId id = nodeInfo.getId();
Name nodeName = nodeInfo.getName();
Name ntName = nodeInfo.getNodeTypeName();
Name[] mixins = nodeInfo.getMixinNames();
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;
}
if (parent.getDefinition().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 (ProtectedItemImporter pni : pItemImporters) {
if (pni instanceof ProtectedNodeImporter && ((ProtectedNodeImporter) pni).start(parent)) {
log.debug("Protected node -> delegated to ProtectedNodeImporter");
pnImporter = (ProtectedNodeImporter) 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.hasNode(nodeName)) {
// a node with that name already exists...
NodeImpl existing = parent.getNode(nodeName);
NodeDefinition def = existing.getDefinition();
if (!def.allowsSameNameSiblings()) {
// existing doesn't allow same-name siblings,
// check for potential conflicts
if (def.isProtected() && existing.isNodeType(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);
return;
}
if (def.isAutoCreated() && existing.isNodeType(ntName)) {
// this node has already been auto-created, no need to create it
node = existing;
} else {
// edge case: colliding node does have same uuid
// (see http://issues.apache.org/jira/browse/JCR-1128)
if (!(existing.getId().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 (node == null) {
// create node
if (id == null) {
// no potential uuid conflict, always add new node
checkPermission(parent, nodeName);
node = createNode(parent, nodeName, ntName, mixins, null);
} else {
// potential uuid conflict
NodeImpl conflicting;
try {
conflicting = session.getNodeById(id);
} catch (ItemNotFoundException infe) {
conflicting = null;
}
if (conflicting != null) {
// resolve uuid conflict
node = resolveUUIDConflict(parent, conflicting, nodeInfo);
if (node == 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
checkPermission(parent, nodeName);
node = createNode(parent, nodeName, ntName, mixins, id);
}
}
}
// process properties
for (PropInfo pi : propInfos) {
// find applicable definition
QPropertyDefinition def = pi.getApplicablePropertyDef(node.getEffectiveNodeType());
if (def.isProtected()) {
// skip protected property
log.debug("Skipping protected property " + pi.getName());
// notify the ProtectedPropertyImporter.
for (ProtectedItemImporter ppi : pItemImporters) {
if (ppi instanceof ProtectedPropertyImporter && ((ProtectedPropertyImporter) ppi).handlePropInfo(node, 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 {
// regular property -> create the property
createProperty(node, pi, def);
}
}
parents.push(node);
}
/**
* {@inheritDoc}
*/
public void endNode(NodeInfo nodeInfo) throws RepositoryException {
NodeImpl parent = parents.pop();
if (parent == null) {
if (pnImporter != null) {
pnImporter.endChildInfo();
}
} else if (parent.getDefinition().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;
}
}
}
/**
* {@inheritDoc}
*/
public void end() throws RepositoryException {
/**
* adjust references that refer to uuid's which have been mapped to
* newly generated uuid's 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 Property)) {
continue;
}
Property prop = (Property) ref;
// being paranoid...
if (prop.getType() != PropertyType.REFERENCE
&& prop.getType() != PropertyType.WEAKREFERENCE) {
continue;
}
if (prop.isMultiple()) {
Value[] values = prop.getValues();
Value[] newVals = new Value[values.length];
for (int i = 0; i < values.length; i++) {
Value val = values[i];
NodeId original = new NodeId(val.getString());
NodeId adjusted = refTracker.getMappedId(original);
if (adjusted != null) {
newVals[i] = session.getValueFactory().createValue(
session.getNodeById(adjusted),
prop.getType() != PropertyType.REFERENCE);
} else {
// reference doesn't need adjusting, just copy old value
newVals[i] = val;
}
}
prop.setValue(newVals);
} else {
Value val = prop.getValue();
NodeId original = new NodeId(val.getString());
NodeId adjusted = refTracker.getMappedId(original);
if (adjusted != null) {
prop.setValue(session.getNodeById(adjusted).getUUID());
}
}
}
refTracker.clear();
}
}