Index: /home/ntoper/workspace/jackrabbit/java/org/apache/jackrabbit/core/xml/SysViewImporter.java =================================================================== --- /home/ntoper/workspace/jackrabbit/java/org/apache/jackrabbit/core/xml/SysViewImporter.java (revision 0) +++ /home/ntoper/workspace/jackrabbit/java/org/apache/jackrabbit/core/xml/SysViewImporter.java (revision 0) @@ -0,0 +1,534 @@ +/* + * 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 org.apache.jackrabbit.core.BatchedItemOperations; +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.NodeId; +import org.apache.jackrabbit.core.PropertyId; +import org.apache.jackrabbit.core.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.core.nodetype.NodeDef; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.nodetype.PropDef; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.util.ReferenceChangeTracker; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.name.MalformedPathException; +import org.apache.jackrabbit.name.Path; +import org.apache.jackrabbit.name.QName; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.PathNotFoundException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; +import java.util.Iterator; +import java.util.List; +import java.util.Stack; + +/** + * SysViewImporter imports a system view XML document as it is + * into an empty repository (nothing is altered) + * + * It is heavily based on the WorkspaceImporter but doesn't extend it (I would have to put too many methods + * in protected for this; tell me if you would be ok with this change though). Here are the differences: + * 1/ it escapes the isProtected check and doesn't autoCreate nodes. It checks if there are some + * contents in the destination, if yes it throws an exception. + * 2/ You have more control through the constructor than the WorkspaceImporter since + * you specify which BatchedItemOperations you want to use. + * 3/ There are no conflicts/replace so basically, it just creates nodes and so is a lot simpler thant the WorkspaceImporter. + */ +public class SysViewImporter implements Importer { + + private static Logger log = LoggerFactory.getLogger(SysViewImporter.class); + + //TODO comment + private final NodeState importTarget; + private final NodeTypeRegistry ntReg; + private final HierarchyManager hierMgr; + private final BatchedItemOperations itemOps; + private final int uuidBehavior; + private boolean aborted; + private Stack parents; + private static Path ROOT; + static { + try { + Path.PathBuilder builder = new Path.PathBuilder(); + builder.addRoot(); + ROOT = builder.getPath(); + } catch (MalformedPathException e) { + // will not happen. path is always valid + throw new InternalError("Cannot initialize path"); + } + } + + /** + * 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; + //TODO Check if empty but how? + /** + * Creates a new sysViewImporter instance. + * + * @param parentPath target path where to add the imported subtree + * @param hierMgr the HierarchyManager of the Repository + * @param ntReg the NodeTypeRegistry of the repository + * @param itemOps BatchedItemOperations to use + * @param uuidBehavior flag that governs how incoming UUIDs are handled + * @throws PathNotFoundException if no node exists at + * parentPath or if the + * current session is not granted read + * access. + * @throws ConstraintViolationException if the node at + * parentPath is protected + * @throws VersionException if the node at + * parentPath is not + * checked-out + * @throws LockException if a lock prevents the addition of + * the subtree + * @throws RepositoryException if another error occurs + */ + public SysViewImporter(Path parentPath, + HierarchyManager hierMgr, + NodeTypeRegistry ntReg, + int uuidBehavior, BatchedItemOperations itemOps) + throws PathNotFoundException, ConstraintViolationException, + VersionException, LockException, RepositoryException { + + this.itemOps = itemOps; + this.hierMgr = hierMgr; + importTarget = itemOps.getNodeState(parentPath); + this.ntReg = ntReg; + this.uuidBehavior = uuidBehavior; + aborted = false; + refTracker = new ReferenceChangeTracker(); + parents = new Stack(); + parents.push(importTarget); + } + + /** + * @param parent the parent NodeState + * @param conflicting the conflicting one + * @param nodeInfo the NodeInfo of the Node to add. + * @return NodeState without conflict + * @throws RepositoryException in case of any exception raised. + */ + protected NodeState resolveUUIDConflict(NodeState parent, + NodeState conflicting, + NodeInfo nodeInfo) + throws RepositoryException { + + NodeState node; + if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW) { + // create new with new uuid: + // check if new node can be added (check access rights & + // node type constraints only, assume locking & versioning status + // has already been checked on ancestor) + itemOps.checkAddNode(parent, nodeInfo.getName(), + nodeInfo.getNodeTypeName(), + BatchedItemOperations.CHECK_ACCESS + | BatchedItemOperations.CHECK_CONSTRAINTS); + node = itemOps.createNodeState(parent, nodeInfo.getName(), + nodeInfo.getNodeTypeName(), nodeInfo.getMixinNames(), null); + // remember uuid mapping + EffectiveNodeType ent = itemOps.getEffectiveNodeType(node); + if (ent.includesNodeType(QName.MIX_REFERENCEABLE)) { + refTracker.mappedUUID(nodeInfo.getId().getUUID(), node.getNodeId().getUUID()); + } + } else if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW) { + 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 importTarget or an ancestor thereof + Path p0 = hierMgr.getPath(importTarget.getNodeId()); + Path p1 = hierMgr.getPath(conflicting.getNodeId()); + try { + if (p1.equals(p0) || p1.isAncestorOf(p0)) { + String msg = "cannot remove ancestor node"; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } catch (MalformedPathException mpe) { + // should never get here... + String msg = "internal error: failed to determine degree of relationship"; + log.error(msg, mpe); + throw new RepositoryException(msg, mpe); + } + // remove conflicting: + // check if conflicting can be removed + // (access rights, node type constraints, locking & versioning status) + itemOps.checkRemoveNode(conflicting, + BatchedItemOperations.CHECK_ACCESS + | BatchedItemOperations.CHECK_LOCK + | BatchedItemOperations.CHECK_VERSIONING + | BatchedItemOperations.CHECK_CONSTRAINTS); + // do remove conflicting (recursive) + itemOps.removeNodeState(conflicting); + + // create new with given uuid: + // check if new node can be added (check access rights & + // node type constraints only, assume locking & versioning status + // has already been checked on ancestor) + itemOps.checkAddNode(parent, nodeInfo.getName(), + nodeInfo.getNodeTypeName(), + BatchedItemOperations.CHECK_ACCESS + | BatchedItemOperations.CHECK_CONSTRAINTS); + // do create new node + node = itemOps.createNodeState(parent, nodeInfo.getName(), + nodeInfo.getNodeTypeName(), nodeInfo.getMixinNames(), + nodeInfo.getId()); + } else if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING) { + NodeId parentId = conflicting.getParentId(); + if (parentId == null) { + String msg = "root node cannot be replaced"; + log.debug(msg); + throw new RepositoryException(msg); + } + // 'replace' current parent with parent of conflicting + try { + parent = itemOps.getNodeState(parentId); + } catch (ItemNotFoundException infe) { + // should never get here... + String msg = "internal error: failed to retrieve parent state"; + log.error(msg, infe); + throw new RepositoryException(msg, infe); + } + // remove conflicting: + // check if conflicting can be removed + // (access rights, node type constraints, locking & versioning status) + itemOps.checkRemoveNode(conflicting, + BatchedItemOperations.CHECK_ACCESS + | BatchedItemOperations.CHECK_LOCK + | BatchedItemOperations.CHECK_VERSIONING + | BatchedItemOperations.CHECK_CONSTRAINTS); + // do remove conflicting (recursive) + itemOps.removeNodeState(conflicting); + // create new with given uuid at same location as conflicting: + // check if new node can be added at other location + // (access rights, node type constraints, locking & versioning status) + itemOps.checkAddNode(parent, nodeInfo.getName(), + nodeInfo.getNodeTypeName(), + BatchedItemOperations.CHECK_ACCESS + | BatchedItemOperations.CHECK_LOCK + | BatchedItemOperations.CHECK_VERSIONING + | BatchedItemOperations.CHECK_CONSTRAINTS); + // do create new node + node = itemOps.createNodeState(parent, nodeInfo.getName(), + nodeInfo.getNodeTypeName(), nodeInfo.getMixinNames(), + nodeInfo.getId()); + } else { + String msg = "unknown uuidBehavior: " + uuidBehavior; + log.debug(msg); + throw new RepositoryException(msg); + } + + return node; + } + + //-------------------------------------------------------------< Importer > + /** + * {@inheritDoc} + */ + public void start() throws RepositoryException { + try { + // start update operation + itemOps.edit(); + } catch (IllegalStateException ise) { + aborted = true; + String msg = "internal error: failed to start update operation"; + log.debug(msg); + throw new RepositoryException(msg, ise); + } + } + + /** + * {@inheritDoc} + */ + public void startNode(NodeInfo nodeInfo, List propInfos) + throws RepositoryException { + if (aborted) { + // the import has been aborted, get outta here... + return; + } + + if ((nodeInfo.getName().equals(new QName("http://www.jcp.org/jcr/1.0", "root")) + || (nodeInfo.getName().equals(new QName("http://www.jcp.org/jcr/1.0", "versionStorage"))))) { + return; + } + + boolean succeeded = false; + NodeState parent; + try { + parent = (NodeState) parents.peek(); + + // process node + NodeState node = null; + NodeId id = nodeInfo.getId(); + QName nodeName = nodeInfo.getName(); + QName ntName = nodeInfo.getNodeTypeName(); + QName[] mixins = nodeInfo.getMixinNames(); + + if (parent == null) { + // parent node was skipped, skip this child node also + parents.push(null); // push null onto stack for skipped node + succeeded = true; + log.debug("skipping node " + nodeName); + return; + } + if (parent.hasChildNodeEntry(nodeName)) { + // a node with that name already exists... + NodeState.ChildNodeEntry entry = + parent.getChildNodeEntry(nodeName, 1); + NodeId idExisting = entry.getId(); + NodeState existing = (NodeState) itemOps.getItemState(idExisting); + NodeDef def = ntReg.getNodeDef(existing.getDefinitionId()); + + if (!def.allowsSameNameSiblings()) { + // existing doesn't allow same-name siblings, + // check for potential conflicts + EffectiveNodeType entExisting = + itemOps.getEffectiveNodeType(existing); + if (def.isAutoCreated() && entExisting.includesNodeType(ntName)) { + // this node has already been auto-created, + // no need to create it + node = existing; + } else { + throw new ItemExistsException(itemOps.safeGetJCRPath(existing.getNodeId())); + } + } + } + + if (node == null) { + // there's no node with that name... + if (id == null) { + // no potential uuid conflict, always create new node + + NodeDef def = + itemOps.findApplicableNodeDefinition(nodeName, ntName, parent); + + if (parent.hasPropertyName(nodeName)) { + /** + * a property with the same name already exists; if this property + * has been imported as well (e.g. through document view import + * where an element can have the same name as one of the attributes + * of its parent element) we have to rename the onflicting property; + * + * see http://issues.apache.org/jira/browse/JCR-61 + */ + PropertyId propId = new PropertyId(parent.getNodeId(), nodeName); + PropertyState conflicting = itemOps.getPropertyState(propId); + if (conflicting.getStatus() == ItemState.STATUS_NEW) { + // assume this property has been imported as well; + // rename conflicting property + // @todo use better reversible escaping scheme to create unique name + QName newName = new QName(nodeName.getNamespaceURI(), nodeName.getLocalName() + "_"); + if (parent.hasPropertyName(newName)) { + newName = new QName(newName.getNamespaceURI(), newName.getLocalName() + "_"); + } + PropertyState newProp = + itemOps.createPropertyState(parent, newName, + conflicting.getType(), conflicting.getValues().length); + newProp.setValues(conflicting.getValues()); + parent.removePropertyName(nodeName); + itemOps.store(parent); + itemOps.destroy(conflicting); + } + } + + // do create new node + node = itemOps.createNodeState(parent, nodeName, ntName, mixins, null, def); + } else { + // potential uuid conflict + NodeState conflicting; + + try { + conflicting = itemOps.getNodeState(id); + } catch (ItemNotFoundException infe) { + conflicting = null; + } + if (conflicting != null) { + // resolve uuid conflict + node = resolveUUIDConflict(parent, conflicting, nodeInfo); + } else { + // create new with given uuid + NodeDef def = + itemOps.findApplicableNodeDefinition(nodeName, ntName, parent); + // do create new node + node = itemOps.createNodeState(parent, nodeName, ntName, mixins, id, def); + } + } + } + + // process properties + Iterator iter = propInfos.iterator(); + while (iter.hasNext()) { + PropInfo pi = (PropInfo) iter.next(); + RestorePropInfo rpi = new RestorePropInfo(pi); + rpi.applyRestore(node, itemOps, ntReg, refTracker); + } + + // store affected nodes + itemOps.store(node); + itemOps.store(parent); + + // push current node onto stack of parents + parents.push(node); + succeeded = true; + } finally { + if (!succeeded) { + // update operation failed, cancel all modifications + aborted = true; + itemOps.cancel(); + } + } + } + + /** + * {@inheritDoc} + */ + public void endNode(NodeInfo nodeInfo) throws RepositoryException { + if (aborted) { + // the import has been aborted, get outta here... + return; + } + if ((nodeInfo.getName().equals(new QName("http://www.jcp.org/jcr/1.0", "root")) + || (nodeInfo.getName().equals(new QName("http://www.jcp.org/jcr/1.0", "versionStorage"))))) { + return; + } + NodeState node = (NodeState) parents.pop(); + if (node == null) { + // node was skipped, nothing to do here + return; + } + boolean succeeded = false; + try { + // we're done with that node, now store its state + itemOps.store(node); + succeeded = true; + } finally { + if (!succeeded) { + // update operation failed, cancel all modifications + aborted = true; + itemOps.cancel(); + } + } + } + + /** + * {@inheritDoc} + */ + public void end() throws RepositoryException { + if (aborted) { + // the import has been aborted, get outta here... + return; + } + + boolean succeeded = false; + try { + refTracker.clear(); + // finally store the state of the import target + // (the parent of the imported subtree) + itemOps.store(importTarget); + succeeded = true; + } finally { + if (!succeeded) { + // update operation failed, cancel all modifications + aborted = true; + itemOps.cancel(); + } + } + + if (!aborted) { + // finish update + itemOps.update(); + } + } + + /** + * + * Class including a regular PropInfo. It is used + * to avoid autocreate properties. + * + */ + public class RestorePropInfo{ + + private final PropInfo pi; + + public RestorePropInfo(PropInfo pi) { + this.pi = pi; + } + + public void applyRestore(NodeState node, BatchedItemOperations itemOps, NodeTypeRegistry ntReg, ReferenceChangeTracker refTracker) throws ItemNotFoundException, RepositoryException { + PropertyState prop = null; + PropDef def = null; + + if (node.hasPropertyName(pi.getName())) { + // a property with that name already exists... + PropertyId idExisting = new PropertyId(node.getNodeId(), pi.getName()); + prop = (PropertyState) itemOps.getItemState(idExisting); + def = ntReg.getPropDef(prop.getDefinitionId()); + if (!def.isAutoCreated() + || (prop.getType() != pi.getType() && pi.getType() != PropertyType.UNDEFINED) + || def.isMultiple() != prop.isMultiValued()) { + throw new ItemExistsException(itemOps.safeGetJCRPath(prop.getPropertyId())); + } + } else { + // there's no property with that name, + // find applicable definition + def = pi.getApplicablePropertyDef(itemOps.getEffectiveNodeType(node)); + + // create new property + prop = itemOps.createPropertyState(node, pi.getName(), pi.getType(), def); + } + + // check multi-valued characteristic + if (pi.getValues().length != 1 && !def.isMultiple()) { + throw new ConstraintViolationException(itemOps.safeGetJCRPath(prop.getPropertyId()) + + " is not multi-valued"); + } + + // convert serialized values to InternalValue objects + int targetType = pi.getTargetType(def); + InternalValue[] iva = new InternalValue[pi.getValues().length]; + for (int i = 0; i < pi.getValues().length; i++) { + iva[i] = pi.getValues()[i].getInternalValue(targetType); + } + + // set values + prop.setValues(iva); + + if (prop.getType() == PropertyType.REFERENCE) { + // store reference for later resolution + refTracker.processedReference(prop); + } + + // store property + itemOps.store(prop); + } + } +}