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);
+ }
+ }
+}