| /************************************************************************* |
| * 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.vault.fs.impl.io; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.UUID; |
| |
| import javax.jcr.ImportUUIDBehavior; |
| import javax.jcr.Item; |
| import javax.jcr.ItemExistsException; |
| import javax.jcr.ItemNotFoundException; |
| import javax.jcr.NamespaceException; |
| import javax.jcr.Node; |
| import javax.jcr.NodeIterator; |
| import javax.jcr.PathNotFoundException; |
| import javax.jcr.Property; |
| import javax.jcr.PropertyIterator; |
| import javax.jcr.PropertyType; |
| import javax.jcr.ReferentialIntegrityException; |
| import javax.jcr.Repository; |
| import javax.jcr.RepositoryException; |
| import javax.jcr.Session; |
| import javax.jcr.Value; |
| import javax.jcr.ValueFactory; |
| import javax.jcr.lock.LockException; |
| import javax.jcr.nodetype.ConstraintViolationException; |
| import javax.jcr.nodetype.NoSuchNodeTypeException; |
| import javax.jcr.nodetype.NodeDefinition; |
| import javax.jcr.nodetype.NodeType; |
| import javax.jcr.nodetype.PropertyDefinition; |
| import javax.jcr.version.VersionException; |
| |
| import org.apache.jackrabbit.spi.Name; |
| import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; |
| import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; |
| import org.apache.jackrabbit.spi.commons.name.NameConstants; |
| import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; |
| import org.apache.jackrabbit.util.Text; |
| import org.apache.jackrabbit.vault.fs.api.Artifact; |
| import org.apache.jackrabbit.vault.fs.api.ArtifactType; |
| import org.apache.jackrabbit.vault.fs.api.IdConflictPolicy; |
| import org.apache.jackrabbit.vault.fs.api.ImportInfo.Info; |
| import org.apache.jackrabbit.vault.fs.api.ImportInfo.Type; |
| import org.apache.jackrabbit.vault.fs.api.ImportMode; |
| import org.apache.jackrabbit.vault.fs.api.ItemFilterSet; |
| import org.apache.jackrabbit.vault.fs.api.NodeNameList; |
| import org.apache.jackrabbit.vault.fs.api.SerializationType; |
| import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter; |
| import org.apache.jackrabbit.vault.fs.impl.ArtifactSetImpl; |
| import org.apache.jackrabbit.vault.fs.impl.PropertyValueArtifact; |
| import org.apache.jackrabbit.vault.fs.io.AccessControlHandling; |
| import org.apache.jackrabbit.vault.fs.io.DocViewParserHandler; |
| import org.apache.jackrabbit.vault.fs.spi.ACLManagement; |
| import org.apache.jackrabbit.vault.fs.spi.ServiceProviderFactory; |
| import org.apache.jackrabbit.vault.fs.spi.UserManagement; |
| import org.apache.jackrabbit.vault.fs.spi.impl.jcr20.JackrabbitUserManagement; |
| import org.apache.jackrabbit.vault.fs.spi.impl.jcr20.JcrNamespaceHelper; |
| import org.apache.jackrabbit.vault.util.DocViewNode2; |
| import org.apache.jackrabbit.vault.util.DocViewProperty2; |
| import org.apache.jackrabbit.vault.util.EffectiveNodeType; |
| import org.apache.jackrabbit.vault.util.JcrConstants; |
| import org.apache.jackrabbit.vault.util.MimeTypes; |
| import org.apache.jackrabbit.vault.util.PathUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.xml.sax.ContentHandler; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.helpers.AttributesImpl; |
| |
| /** |
| * Imports nodes represented by {@link DocViewNode2} into the repository. |
| */ |
| public class DocViewImporter implements DocViewParserHandler { |
| |
| public static final String ATTRIBUTE_TYPE_CDATA = "CDATA"; |
| private static final Name NAME_REP_CUG_POLICY = NameFactoryImpl.getInstance().create(Name.NS_REP_URI, "cugPolicy"); |
| private static final Name NAME_REP_MEMBERS = NameFactoryImpl.getInstance().create(Name.NS_REP_URI, "members"); |
| |
| private static final String NAMESPACE_OAK = "http://jackrabbit.apache.org/oak/ns/1.0"; |
| private static final Name NAME_OAK_COUNTER = NameFactoryImpl.getInstance().create(NAMESPACE_OAK, "counter"); |
| |
| /** |
| * the default logger |
| */ |
| static final Logger log = LoggerFactory.getLogger(DocViewImporter.class); |
| |
| /** |
| * these properties are protected but are set for new nodes nevertheless via system view xml import |
| */ |
| static final Set<Name> PROTECTED_PROPERTIES_CONSIDERED_FOR_NEW_NODES; |
| |
| /** |
| * these properties are protected but are set for updated nodes via special JCR methods |
| */ |
| static final Set<Name> PROTECTED_PROPERTIES_CONSIDERED_FOR_UPDATED_NODES; |
| |
| static { |
| Set<Name> props = new HashSet<>(); |
| props.add(NameConstants.JCR_PRIMARYTYPE); |
| props.add(NameConstants.JCR_MIXINTYPES); |
| props.add(NameConstants.JCR_UUID); |
| PROTECTED_PROPERTIES_CONSIDERED_FOR_UPDATED_NODES = Collections.unmodifiableSet(props); |
| props.add(NameConstants.JCR_ISCHECKEDOUT); |
| props.add(NameConstants.JCR_BASEVERSION); |
| props.add(NameConstants.JCR_PREDECESSORS); |
| props.add(NameConstants.JCR_SUCCESSORS); |
| props.add(NameConstants.JCR_VERSIONHISTORY); |
| PROTECTED_PROPERTIES_CONSIDERED_FOR_NEW_NODES = Collections.unmodifiableSet(props); |
| } |
| |
| |
| /** |
| * the importing session |
| */ |
| private final Session session; |
| |
| /** |
| * import information for the nodes touched through this DocView (initially empty) |
| */ |
| private ImportInfoImpl importInfo = new ImportInfoImpl(); |
| |
| /** |
| * Specified the filter that is used to check if child nodes are contained |
| * in the import or not. |
| */ |
| private final ItemFilterSet filter; |
| |
| /** |
| * the workspace filter |
| */ |
| private final WorkspaceFilter wspFilter; |
| |
| /** |
| * a map of binaries (attachments) |
| */ |
| private Map<String, Map<String, BlobInfo>> binaries |
| = new HashMap<>(); |
| |
| /** |
| * map of hint nodes in the same artifact set |
| */ |
| private Set<String> hints = new HashSet<>(); |
| |
| /** |
| * properties that should not be deleted on existing nodes in the repository |
| */ |
| private Set<String> preserveProperties = new HashSet<>(); |
| |
| /** |
| * acl management |
| */ |
| private final ACLManagement aclManagement; |
| |
| /** |
| * user management |
| */ |
| private final UserManagement userManagement; |
| |
| /** |
| * the acl handling to apply |
| */ |
| private final AccessControlHandling aclHandling; |
| |
| /** |
| * Closed user group handling to apply by default (when set to <code>null</code>) |
| * falls back to using aclHandling |
| */ |
| private final @Nullable AccessControlHandling cugHandling; |
| |
| /** |
| * helper for namespace registration |
| */ |
| private final JcrNamespaceHelper nsHelper; |
| |
| private final IdConflictPolicy idConflictPolicy; |
| |
| /** |
| * current stack |
| */ |
| private StackElement stack; |
| |
| /** |
| * the current namespace state |
| */ |
| private DocViewSAXHandler.Namespace nsStack = null; |
| |
| private int rootDepth; |
| |
| private final NamePathResolver npResolver; |
| /** |
| * {@code true} in case the repository supports same-name siblings |
| */ |
| private final boolean isSnsSupported; |
| |
| /** |
| * Creates a new importer that will imports the |
| * items below the given root. |
| * |
| * @param parentNode the (parent) node of the import |
| * @param rootNodeName name of the root node |
| * @param artifacts the artifact set that could contain attachments |
| * @param wspFilter workspace filter |
| * @throws RepositoryException if an error occurs. |
| */ |
| public DocViewImporter(Node parentNode, String rootNodeName, |
| ArtifactSetImpl artifacts, WorkspaceFilter wspFilter, IdConflictPolicy idConflictPolicy) throws RepositoryException { |
| this(parentNode, rootNodeName, artifacts, wspFilter, idConflictPolicy, AccessControlHandling.IGNORE, null); |
| } |
| |
| public DocViewImporter(Node parentNode, String rootNodeName, |
| ArtifactSetImpl artifacts, WorkspaceFilter wspFilter, IdConflictPolicy idConflictPolicy, AccessControlHandling aclHandling, AccessControlHandling cugHandling) throws RepositoryException { |
| this.filter = artifacts.getCoverage(); |
| this.wspFilter = wspFilter; |
| this.rootDepth = parentNode.getDepth() + 1; |
| this.session = parentNode.getSession(); |
| this.aclManagement = ServiceProviderFactory.getProvider().getACLManagement(); |
| this.userManagement = ServiceProviderFactory.getProvider().getUserManagement(); |
| this.nsHelper = new JcrNamespaceHelper(session, null); |
| this.idConflictPolicy = idConflictPolicy; |
| this.aclHandling = aclHandling; |
| this.cugHandling = cugHandling; |
| this.isSnsSupported = session.getRepository(). |
| getDescriptorValue(Repository.NODE_TYPE_MANAGEMENT_SAME_NAME_SIBLINGS_SUPPORTED).getBoolean(); |
| |
| String rootPath = parentNode.getPath(); |
| if (!rootPath.equals("/")) { |
| rootPath += "/"; |
| } |
| for (Artifact a : artifacts.values(ArtifactType.BINARY)) { |
| registerBinary(a, rootPath); |
| } |
| for (Artifact a : artifacts.values(ArtifactType.FILE)) { |
| if (a.getSerializationType() != SerializationType.XML_DOCVIEW) { |
| registerBinary(a, rootPath); |
| } |
| } |
| for (Artifact a : artifacts.values(ArtifactType.HINT)) { |
| hints.add(rootPath + a.getRelativePath()); |
| } |
| |
| stack = new StackElement(parentNode, parentNode.isNew()); |
| npResolver = new DefaultNamePathResolver(parentNode.getSession()); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * <p> |
| * Pushes the mapping to the stack and updates the namespace mapping in the |
| * session. |
| */ |
| @Override |
| public void startPrefixMapping(String prefix, String uri) { |
| // for backwards compatibility unknown namespaces in the repository need to be registered because some API can only deal with qualified/prefixed names |
| log.trace("-> prefixMapping for {}:{}", prefix, uri); |
| DocViewSAXHandler.Namespace ns = new DocViewSAXHandler.Namespace(prefix, uri); |
| // push on stack |
| ns.next = nsStack; |
| nsStack = ns; |
| // check if uri is already registered |
| String oldPrefix; |
| try { |
| oldPrefix = session.getNamespacePrefix(uri); |
| } catch (NamespaceException e) { |
| // assume uri never registered |
| try { |
| oldPrefix = nsHelper.registerNamespace(prefix, uri); |
| } catch (RepositoryException e1) { |
| throw new IllegalStateException(e1); |
| } |
| } catch (RepositoryException e) { |
| throw new IllegalStateException(e); |
| } |
| // update mapping |
| if (!oldPrefix.equals(prefix)) { |
| try { |
| session.setNamespacePrefix(prefix, uri); |
| } catch (RepositoryException e) { |
| throw new IllegalStateException(e); |
| } |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| * <p> |
| * Pops the mapping from the stack and updates the namespace mapping in the |
| * session if necessary. |
| */ |
| @Override |
| public void endPrefixMapping(String prefix) { |
| log.trace("<- prefixMapping for {}", prefix); |
| DocViewSAXHandler.Namespace ns = nsStack; |
| DocViewSAXHandler.Namespace prev = null; |
| while (ns != null && !ns.prefix.equals(prefix)) { |
| prev = ns; |
| ns = ns.next; |
| } |
| if (ns == null) { |
| throw new IllegalStateException("Illegal state: prefix " + prefix + " never mapped."); |
| } |
| // remove from stack |
| if (prev == null) { |
| nsStack = ns.next; |
| } else { |
| prev.next = ns.next; |
| } |
| // find old prefix |
| ns = ns.next; |
| while (ns != null && !ns.prefix.equals(prefix)) { |
| ns = ns.next; |
| } |
| // update mapping |
| if (ns != null) { |
| try { |
| session.setNamespacePrefix(prefix, ns.uri); |
| } catch (RepositoryException e) { |
| throw new IllegalStateException(e); |
| } |
| log.trace(" remapped: {}:{}", prefix, ns.uri); |
| } |
| } |
| |
| @Override |
| public void startDocViewNode(@NotNull String nodePath, @NotNull DocViewNode2 docViewNode, @NotNull Optional<DocViewNode2> parentDocViewNode, int line, int column) throws IOException, RepositoryException { |
| stack.addName(docViewNode.getSnsAwareName()); |
| Node node = stack.getNode(); |
| if (node == null) { |
| stack = stack.push(); |
| DocViewAdapter xform = stack.getAdapter(); |
| if (xform != null) { |
| xform.startNode(docViewNode); |
| } else { |
| log.trace("Skipping ignored node {}", docViewNode); // TODO: clarify what this means |
| } |
| } else { |
| if (docViewNode.getProperties().isEmpty()) { |
| // only ordering node. skip |
| log.trace("Skipping empty node {}", nodePath); |
| stack = stack.push(); |
| return; |
| } else if (docViewNode.getIndex() > 1 && !isSnsSupported) { |
| //skip SNS nodes with index > 1 |
| log.warn("Skipping unsupported SNS node with index > 1. Some content will be missing after import: {}", nodePath); |
| stack = stack.push(); |
| return; |
| } |
| try { |
| // is policy node? |
| if (docViewNode.getPrimaryType().filter(aclManagement::isACLNodeType).isPresent()) { |
| AccessControlHandling acHandling = getAcHandling(docViewNode.getName()); |
| if (acHandling != AccessControlHandling.CLEAR && acHandling != AccessControlHandling.IGNORE) { |
| log.trace("Access control policy element detected. starting special transformation {}/{}", node.getPath(), docViewNode.getName()); |
| if (aclManagement.ensureAccessControllable(node, npResolver.getJCRName(docViewNode.getName()))) { |
| log.debug("Adding access control policy element to non access-controllable parent - adding mixin: {}", node.getPath()); |
| } |
| stack = stack.push(); |
| if (NameConstants.REP_REPO_POLICY.equals(docViewNode.getName())) { |
| if (node.getDepth() == 0) { |
| stack.adapter = new JackrabbitACLImporter(session, acHandling); |
| stack.adapter.startNode(docViewNode); |
| } else { |
| log.debug("ignoring invalid location for repository level ACL: {}", node.getPath()); |
| } |
| } else { |
| |
| stack.adapter = new JackrabbitACLImporter(node, acHandling); |
| stack.adapter.startNode(docViewNode); |
| } |
| } else { |
| stack = stack.push(); |
| } |
| } else if (userManagement != null && docViewNode.getPrimaryType().filter(userManagement::isAuthorizableNodeType).isPresent()) { |
| // is authorizable node? |
| handleAuthorizable(node, docViewNode); |
| } else { |
| // regular node |
| stack = stack.push(addNode(docViewNode)); |
| } |
| } catch (RepositoryException | IOException e) { |
| if (e instanceof ConstraintViolationException && wspFilter.getImportMode(nodePath) != ImportMode.REPLACE) { |
| // only warn in case of constraint violations for mode != replace (as best effort is used in that case) |
| log.warn("Error during processing of {}: {}, skip node due to import mode {}", nodePath, e.toString(), wspFilter.getImportMode(nodePath)); |
| importInfo.onNop(nodePath); |
| } else { |
| log.error("Error during processing of {}: {}", nodePath, e.toString()); |
| importInfo.onError(nodePath, e); |
| } |
| stack = stack.push(); |
| } |
| } |
| } |
| |
| |
| @Override |
| public void endDocViewNode(@NotNull String nodePath, @NotNull DocViewNode2 docViewNode, @NotNull Optional<DocViewNode2> parentDocViewNode, int line, int column) throws IOException, RepositoryException { |
| // currentNode's import is finished, check if any child nodes |
| // need to be removed |
| NodeNameList childNames = stack.getChildNames(); |
| Node node = stack.getNode(); |
| int numChildren = 0; |
| if (node == null) { |
| DocViewAdapter adapter = stack.getAdapter(); |
| if (adapter != null) { |
| adapter.endNode(); |
| } |
| // close transformer if last in stack |
| if (stack.adapter != null) { |
| List<String> createdPaths = stack.adapter.close(); |
| for (String createdPath : createdPaths) { |
| importInfo.onCreated(createdPath); |
| } |
| stack.adapter = null; |
| log.trace("Sysview transformation complete."); |
| } |
| } else { |
| NodeIterator iter = node.getNodes(); |
| while (iter.hasNext()) { |
| numChildren++; |
| Node child = iter.nextNode(); |
| String path = child.getPath(); |
| String label = Text.getName(path); |
| AccessControlHandling acHandling = getAcHandling(npResolver.getQName(child.getName())); |
| if (!childNames.contains(label) |
| && !hints.contains(path) |
| && isIncluded(child, child.getDepth() - rootDepth)) { |
| // if the child is in the filter, it belongs to |
| // this aggregate and needs to be removed |
| if (aclManagement.isACLNode(child)) { |
| if (acHandling == AccessControlHandling.OVERWRITE |
| || acHandling == AccessControlHandling.CLEAR) { |
| importInfo.onDeleted(path); |
| aclManagement.clearACL(node); |
| } |
| } else { |
| if (wspFilter.getImportMode(path) == ImportMode.REPLACE) { |
| boolean shouldRemoveChild = true; |
| // check if child is not protected |
| if (child.getDefinition().isProtected()) { |
| log.warn("Refuse to delete protected child node: {}", path); |
| shouldRemoveChild = false; |
| // check if child is mandatory (and not residual, https://s.apache.org/jcr-2.0-spec/2.0/3_Repository_Model.html#3.7.2.4%20Mandatory) |
| } else if (child.getDefinition().isMandatory() && !child.getDefinition().getName().equals("*")) { |
| // get relevant child node definition from parent's effective node type |
| EffectiveNodeType ent = EffectiveNodeType.ofNode(child.getParent()); |
| Optional<NodeDefinition> childNodeDefinition = ent.getApplicableChildNodeDefinition(child.getName(), child.getPrimaryNodeType()); |
| if (!childNodeDefinition.isPresent()) { |
| // this should never happen as then child.getDefinition().isMandatory() would have returned false in the first place... |
| throw new IllegalStateException("Could not find applicable child node definition for mandatory child node " + child.getPath()); |
| } else { |
| if (!hasSiblingWithPrimaryTypesAndName(child, childNodeDefinition.get().getRequiredPrimaryTypes(), childNodeDefinition.get().getName())) { |
| log.warn("Refuse to delete mandatory non-residual child node: {} with no other matching siblings", path); |
| shouldRemoveChild = false; |
| } |
| } |
| } |
| if (shouldRemoveChild) { |
| importInfo.onDeleted(path); |
| child.remove(); |
| } |
| } |
| } |
| } else if (acHandling == AccessControlHandling.CLEAR |
| && aclManagement.isACLNode(child) |
| && isIncluded(child, child.getDepth() - rootDepth)) { |
| importInfo.onDeleted(path); |
| aclManagement.clearACL(node); |
| } |
| } |
| if (isIncluded(node, node.getDepth() - rootDepth)) { |
| // ensure order |
| stack.restoreOrder(); |
| } |
| } |
| stack = stack.pop(); |
| if (node != null && (numChildren == 0 && !childNames.isEmpty() || stack.isRoot())) { |
| importInfo.addNameList(node.getPath(), childNames); |
| } |
| } |
| |
| private boolean hasSiblingWithPrimaryTypesAndName(Node node, NodeType[] requiredPrimaryNodeTypes, String requiredName) throws RepositoryException { |
| NodeIterator iter = node.getParent().getNodes(); |
| while (iter.hasNext()) { |
| Node sibling = iter.nextNode(); |
| if (!sibling.isSame(node)) { |
| boolean allTypesMatch = true; |
| // check type: due to inheritance multiple primary node types need to be checked |
| for (NodeType requiredPrimaryNodeType : requiredPrimaryNodeTypes) { |
| allTypesMatch &= sibling.isNodeType(requiredPrimaryNodeType.getName()); |
| } |
| // check name |
| if (allTypesMatch && (requiredName.equals("*") || requiredName.equals(node.getName()))) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * @throws RepositoryException |
| * @throws IOException |
| * @throws ConstraintViolationException |
| * @throws VersionException |
| * @throws LockException |
| * @throws NoSuchNodeTypeException |
| * @throws PathNotFoundException |
| * @throws ItemExistsException |
| */ |
| @Override |
| public void endDocument() throws RepositoryException, IOException { |
| if (!stack.isRoot()) { |
| throw new IllegalStateException("stack mismatch"); |
| } |
| |
| // process binaries |
| for (String parentPath : binaries.keySet()) { |
| Map<String, BlobInfo> blobs = binaries.get(parentPath); |
| // check for node |
| log.trace("processing binaries at {}", parentPath); |
| if (session.nodeExists(parentPath)) { |
| Node node = session.getNode(parentPath); |
| for (String propName : blobs.keySet()) { |
| BlobInfo info = blobs.get(propName); |
| if (node.hasNode(propName)) { |
| handleBinNode(node.getNode(propName), info, true); |
| } else if (info.isFile()) { |
| // special case for not existing files |
| Node fNode = node.addNode(propName, JcrConstants.NT_FILE); |
| importInfo.onCreated(fNode.getPath()); |
| handleBinNode(fNode, info, false); |
| } else { |
| if (info.isMulti) { |
| node.setProperty(propName, info.getValues(session)); |
| } else { |
| node.setProperty(propName, info.getValue(session)); |
| } |
| importInfo.onModified(node.getPath()); |
| } |
| } |
| |
| } else { |
| log.warn("binaries parent path does not exist: {}", parentPath); |
| // assume below 'this' root. |
| Node node = null; |
| for (String propName : blobs.keySet()) { |
| BlobInfo info = blobs.get(propName); |
| if (info.isFile()) { |
| if (node == null) { |
| node = createNodeDeep(parentPath); |
| } |
| Node fNode = node.addNode(propName, JcrConstants.NT_FILE); |
| importInfo.onCreated(fNode.getPath()); |
| handleBinNode(fNode, info, false); |
| } |
| } |
| } |
| } |
| } |
| |
| private void registerBinary(Artifact a, String rootPath) |
| throws RepositoryException { |
| String path = rootPath + a.getRelativePath(); |
| final int idx; |
| int pos = path.indexOf('[', path.lastIndexOf('/')); |
| if (pos > 0) { |
| idx = Integer.parseInt(path.substring(pos + 1, path.length() - 1)); |
| path = path.substring(0, pos); |
| } else { |
| idx = -1; |
| } |
| if (a.getType() == ArtifactType.FILE && a instanceof PropertyValueArtifact) { |
| // hack, mark "file" properties just as present |
| String parentPath = ((PropertyValueArtifact) a).getProperty().getParent().getPath(); |
| preserveProperties.add(parentPath + "/" + JcrConstants.JCR_DATA); |
| preserveProperties.add(parentPath + "/" + JcrConstants.JCR_LASTMODIFIED); |
| } else { |
| preserveProperties.add(path); |
| // hack, mark "file" properties just as present |
| preserveProperties.add(path + "/jcr:content/jcr:data"); |
| preserveProperties.add(path + "/jcr:content/jcr:lastModified"); |
| preserveProperties.add(path + "/jcr:content/jcr:mimeType"); |
| String parentPath = Text.getRelativeParent(path, 1); |
| String name = Text.getName(path); |
| Map<String, BlobInfo> infoSet = binaries.computeIfAbsent(parentPath, (p) -> new HashMap<>()); |
| BlobInfo info = infoSet.computeIfAbsent(name, (n) -> new BlobInfo(idx >= 0)); |
| if (idx >= 0) { |
| info.add(idx, a); |
| } else { |
| info.add(a); |
| } |
| } |
| log.trace("scheduling binary: {}{}", rootPath, a.getRelativePath() + a.getExtension()); |
| } |
| |
| private boolean isIncluded(Item item, int depth) throws RepositoryException { |
| String path = importInfo.getRemapped().map(item.getPath()); |
| return wspFilter.contains(path) && (depth == 0 || filter.contains(item, path, depth)); |
| } |
| |
| public ImportInfoImpl getInfo() { |
| return importInfo; |
| } |
| |
| private Node createNodeDeep(String path) throws RepositoryException { |
| if (session.nodeExists(path)) { |
| return session.getNode(path); |
| } |
| int idx = path.lastIndexOf('/'); |
| if (idx <= 0) { |
| return session.getRootNode(); |
| } |
| String parentPath = path.substring(0, idx); |
| String name = path.substring(idx + 1); |
| Node parentNode = createNodeDeep(parentPath); |
| Node node; |
| try { |
| node = parentNode.addNode(name); |
| } catch (RepositoryException e) { |
| // try create with nt:folder |
| node = parentNode.addNode(name, JcrConstants.NT_FOLDER); |
| } |
| importInfo.onCreated(node.getPath()); |
| return node; |
| } |
| |
| private void handleBinNode(Node node, BlobInfo info, boolean checkIfNtFileOk) |
| throws RepositoryException, IOException { |
| log.trace("handling binary file at {}", node.getPath()); |
| if (info.isMulti) { |
| throw new IllegalStateException("unable to add MV binary to node " + node.getPath()); |
| } |
| |
| if (checkIfNtFileOk) { |
| if (node.isNodeType(JcrConstants.NT_FILE)) { |
| if (node.hasNode(JcrConstants.JCR_CONTENT)) { |
| node = node.getNode(JcrConstants.JCR_CONTENT); |
| } else { |
| node = node.addNode(JcrConstants.JCR_CONTENT, JcrConstants.NT_RESOURCE); |
| } |
| } |
| } else { |
| node = node.addNode(JcrConstants.JCR_CONTENT, JcrConstants.NT_RESOURCE); |
| } |
| |
| Artifact a = info.artifacts.get(0); |
| // Keep track of whether this file got modified |
| boolean modified = false; |
| // Set the jcr:data property |
| ValueFactory factory = node.getSession().getValueFactory(); |
| try (InputStream input = a.getInputStream()) { |
| Value value = factory.createValue(input); |
| if (node.hasProperty(JcrConstants.JCR_DATA)) { |
| Property data = node.getProperty(JcrConstants.JCR_DATA); |
| if (!value.equals(data.getValue())) { |
| data.setValue(value); |
| // mark jcr:data as modified. |
| importInfo.onModified(data.getPath()); |
| modified = true; |
| } |
| } else { |
| Property data = node.setProperty(JcrConstants.JCR_DATA, value); |
| // mark jcr:data as created |
| importInfo.onCreated(data.getPath()); |
| modified = true; |
| } |
| } |
| // always update last modified if binary was modified (bug #22969) |
| if (!node.hasProperty(JcrConstants.JCR_LASTMODIFIED) || modified) { |
| Calendar lastModified = Calendar.getInstance(); |
| node.setProperty(JcrConstants.JCR_LASTMODIFIED, lastModified); |
| modified = true; |
| } |
| // do not overwrite mimetype |
| if (!node.hasProperty(JcrConstants.JCR_MIMETYPE)) { |
| String mimeType = a.getContentType(); |
| if (mimeType == null) { |
| mimeType = Text.getName(a.getRelativePath(), '.'); |
| mimeType = MimeTypes.getMimeType(mimeType, MimeTypes.APPLICATION_OCTET_STREAM); |
| } |
| node.setProperty(JcrConstants.JCR_MIMETYPE, mimeType); |
| modified = true; |
| } |
| if (node.isNew()) { |
| importInfo.onCreated(node.getPath()); |
| } else if (modified) { |
| importInfo.onModified(node.getPath()); |
| } |
| } |
| |
| |
| /** |
| * Handle an authorizable node |
| * |
| * @param node the parent node |
| * @param docViewNode doc view node of the authorizable |
| * @throws RepositoryException if an error accessing the repository occurrs. |
| * @throws SAXException if an XML parsing error occurrs. |
| */ |
| private void handleAuthorizable(Node node, DocViewNode2 docViewNode) throws RepositoryException { |
| String id = userManagement.getAuthorizableId(docViewNode); |
| String newPath = node.getPath() + "/" + npResolver.getJCRName(docViewNode.getName()); |
| boolean isIncluded = wspFilter.contains(newPath); |
| String oldPath = userManagement.getAuthorizablePath(this.session, id); |
| if (oldPath == null) { |
| if (!isIncluded) { |
| log.trace("auto-creating authorizable node not in filter {}", newPath); |
| } |
| |
| // just import the authorizable node |
| log.trace("Authorizable element detected. starting sysview transformation {}", newPath); |
| stack = stack.push(); |
| stack.adapter = new JcrSysViewTransformer(node, wspFilter.getImportMode(newPath)); |
| stack.adapter.startNode(docViewNode); |
| importInfo.onCreated(newPath); |
| return; |
| } |
| |
| Node authNode = session.getNode(oldPath); |
| ImportMode mode = wspFilter.getImportMode(newPath); |
| |
| // if existing path is not the same as this, we need to register this so that further |
| // nodes down the line (i.e. profiles, policies) are imported at the correct location |
| // we only follow existing authorizables for non-REPLACE mode and if ignoring this authorizable node |
| // todo: check if this also works cross-aggregates |
| if (mode != ImportMode.REPLACE || !isIncluded) { |
| importInfo.onRemapped(oldPath, newPath); |
| } |
| |
| if (!isIncluded) { |
| // skip authorizable handling - always follow existing authorizable - regardless of mode |
| // todo: we also need to check any rep:Memberlist subnodes. see JCRVLT-69 |
| stack = stack.push(new StackElement(authNode, false)); |
| importInfo.onNop(oldPath); |
| return; |
| } |
| |
| switch (mode) { |
| case MERGE: |
| case MERGE_PROPERTIES: |
| // remember desired memberships. |
| // todo: how to deal with multi-node memberships? see JCRVLT-69 |
| Optional<DocViewProperty2> prop = docViewNode.getProperty(NAME_REP_MEMBERS); |
| if (prop.isPresent()) { |
| importInfo.registerMemberships(id, prop.get().getStringValues().toArray(new String[0])); |
| } |
| |
| log.debug("Skipping import of existing authorizable '{}' due to MERGE import mode.", id); |
| stack = stack.push(new StackElement(authNode, false)); |
| importInfo.onNop(newPath); |
| break; |
| |
| case REPLACE: |
| // just replace the entire subtree for now. |
| log.trace("Authorizable element detected. starting sysview transformation {}", newPath); |
| stack = stack.push(); |
| stack.adapter = new JcrSysViewTransformer(node, mode); |
| stack.adapter.startNode(docViewNode); |
| importInfo.onReplaced(newPath); |
| break; |
| |
| case UPDATE: |
| case UPDATE_PROPERTIES: |
| log.trace("Authorizable element detected. starting sysview transformation {}", newPath); |
| stack = stack.push(); |
| stack.adapter = new JcrSysViewTransformer(node, oldPath, mode); |
| // we need to tweak the ni.name so that the sysview import does not |
| // rename the authorizable node name |
| String newName = Text.getName(oldPath); |
| Collection<DocViewProperty2> properties = new LinkedList<>(docViewNode.getProperties()); |
| // but we need to augment with a potential rep:authorizableId |
| if (authNode.hasProperty("rep:authorizableId")) { |
| DocViewProperty2 authId = new DocViewProperty2( |
| JackrabbitUserManagement.NAME_REP_AUTHORIZABLE_ID, |
| authNode.getProperty("rep:authorizableId").getString(), |
| PropertyType.STRING |
| ); |
| properties.removeIf((p) -> p.getName().equals(JackrabbitUserManagement.NAME_REP_AUTHORIZABLE_ID)); |
| properties.add(authId); |
| } |
| |
| DocViewNode2 mapped = new DocViewNode2( |
| npResolver.getQName(newName), |
| properties |
| ); |
| |
| stack.adapter.startNode(mapped); |
| importInfo.onReplaced(newPath); |
| break; |
| } |
| } |
| |
| private StackElement addNode(DocViewNode2 docViewNode) throws RepositoryException, IOException { |
| final Node currentNode = stack.getNode(); |
| |
| Collection<DocViewProperty2> preprocessedProperties = new LinkedList<>(docViewNode.getProperties()); |
| Node existingNode = null; |
| if (NameConstants.ROOT.equals(docViewNode.getName())) { |
| // special case for root node update |
| existingNode = currentNode; |
| } else { |
| if (stack.checkForNode() && currentNode.hasNode(docViewNode.getName().toString())) { |
| existingNode = currentNode.getNode(docViewNode.getName().toString()); |
| } |
| Optional<String> identifier = docViewNode.getIdentifier(); |
| if (identifier.isPresent()) { |
| try { |
| // does uuid already exist in the repo? |
| Node sameIdNode = session.getNodeByIdentifier(identifier.get()); |
| String newNodePath = currentNode.getPath() + "/" + npResolver.getJCRName(docViewNode.getName()); |
| // edge-case: same node path -> uuid is kept |
| if (existingNode != null && existingNode.getPath().equals(sameIdNode.getPath())) { |
| log.debug("Node at {} with existing identifier {} is being updated without modifying its identifier", existingNode.getPath(), docViewNode.getIdentifier()); |
| } else { |
| log.warn("Node Collision: To-be imported node {} uses a node identifier {} which is already taken by {}, trying to resolve conflict according to policy {}", |
| newNodePath, docViewNode.getIdentifier(), sameIdNode.getPath(), idConflictPolicy.name()); |
| if (idConflictPolicy == IdConflictPolicy.FAIL) { |
| // uuid found in path covered by filter |
| if (isIncluded(sameIdNode, 0)) { |
| Info sameIdNodeInfo = importInfo.getInfo(sameIdNode.getPath()); |
| // is the conflicting node part of the package (i.e. the package contained duplicate uuids) |
| if (sameIdNodeInfo != null && sameIdNodeInfo.getType() != Type.DEL) { |
| throw new ReferentialIntegrityException("Node identifier " + docViewNode.getIdentifier() + " already taken by node " + sameIdNode.getPath() + " from the same package"); |
| } else { |
| log.warn("Trying to remove existing conflicting node {} (and all its references)", sameIdNode.getPath()); |
| removeReferences(sameIdNode); |
| String sameIdNodePath = sameIdNode.getPath(); |
| session.removeItem(sameIdNodePath); |
| log.warn("Node {} and its references removed", sameIdNodePath); |
| } |
| existingNode = null; |
| } else { |
| // uuid found in path not-covered by filter |
| throw new ReferentialIntegrityException("Node identifier " + docViewNode.getIdentifier() + " already taken by node " + sameIdNode.getPath()); |
| } |
| } else if (idConflictPolicy == IdConflictPolicy.LEGACY) { |
| // is the conflicting node a sibling |
| if (sameIdNode.getParent().isSame(currentNode)) { |
| String sameIdNodePath = sameIdNode.getPath(); |
| if (isIncluded(sameIdNode, 0)) { |
| log.warn("Existing conflicting node {} has same parent as to-be imported one and is contained in the filter, trying to remove it.", sameIdNodePath); |
| session.removeItem(sameIdNodePath); // references point to new node afterwards |
| importInfo.onDeleted(sameIdNodePath); |
| } else { |
| log.warn("Existing conflicting node {} has same parent as to-be imported one and is not contained in the filter, ignoring new node but continue with children below existing conflicting node", sameIdNodePath); |
| importInfo.onRemapped(newNodePath, sameIdNodePath); |
| existingNode = sameIdNode; |
| } |
| } else { |
| log.warn("To-be imported node and existing conflicting node have different parents. Will create new identifier for the former. ({})", |
| newNodePath); |
| preprocessedProperties.removeIf(p -> p.getName().equals(NameConstants.JCR_UUID) |
| || p.getName().equals(NameConstants.JCR_BASEVERSION) |
| || p.getName().equals(NameConstants.JCR_PREDECESSORS) |
| || p.getName().equals(NameConstants.JCR_SUCCESSORS) |
| || p.getName().equals(NameConstants.JCR_VERSIONHISTORY)); |
| } |
| } |
| } |
| } catch (ItemNotFoundException e) { |
| // ignore |
| } |
| } |
| } |
| |
| // check if new node needs to be checked in |
| preprocessedProperties.removeIf(p -> p.getName().equals(NameConstants.JCR_ISCHECKEDOUT)); |
| boolean isCheckedIn = "false".equals(docViewNode.getPropertyValue(NameConstants.JCR_ISCHECKEDOUT).orElse("true")); |
| |
| // create or update node |
| boolean isNew = existingNode == null; |
| if (isNew) { |
| // workaround for bug in jcr2spi if mixins are empty |
| if (!docViewNode.hasProperty(NameConstants.JCR_MIXINTYPES)) { |
| preprocessedProperties.add(new DocViewProperty2(NameConstants.JCR_MIXINTYPES, Collections.emptyList(), PropertyType.NAME)); |
| } |
| |
| stack.ensureCheckedOut(); |
| existingNode = createNewNode(currentNode, docViewNode.cloneWithDifferentProperties(preprocessedProperties)); |
| if (existingNode.getDefinition() == null) { |
| throw new RepositoryException("Child node not allowed."); |
| } |
| if (existingNode.isNodeType(JcrConstants.NT_RESOURCE)) { |
| if (!existingNode.hasProperty(JcrConstants.JCR_DATA)) { |
| importInfo.onMissing(existingNode.getPath() + "/" + JcrConstants.JCR_DATA); |
| } |
| } else if (isCheckedIn) { |
| // don't rely on isVersionable here, since SPI might not have this info yet |
| importInfo.registerToVersion(existingNode.getPath()); |
| } |
| importInfo.onCreated(existingNode.getPath()); |
| |
| } else if (isIncluded(existingNode, existingNode.getDepth() - rootDepth)) { |
| if (isCheckedIn) { |
| // don't rely on isVersionable here, since SPI might not have this info yet |
| importInfo.registerToVersion(existingNode.getPath()); |
| } |
| ImportMode importMode = wspFilter.getImportMode(existingNode.getPath()); |
| Node updatedNode = updateExistingNode(existingNode, docViewNode.cloneWithDifferentProperties(preprocessedProperties), importMode); |
| if (updatedNode != null) { |
| if (updatedNode.isNodeType(JcrConstants.NT_RESOURCE) && !updatedNode.hasProperty(JcrConstants.JCR_DATA)) { |
| importInfo.onMissing(existingNode.getPath() + "/" + JcrConstants.JCR_DATA); |
| } |
| importInfo.onModified(updatedNode.getPath()); |
| existingNode = updatedNode; |
| } else { |
| importInfo.onNop(existingNode.getPath()); |
| } |
| } else { |
| // remove registered binaries outside of the filter (JCR-126) |
| binaries.remove(existingNode.getPath()); |
| } |
| return new StackElement(existingNode, isNew); |
| } |
| |
| /** |
| * Tries to remove references to the given node but only in case they are included in the filters. |
| * @param node the referenced node |
| * @throws ReferentialIntegrityException in case some references can not be removed (outside filters) |
| * @throws RepositoryException in case some other error occurs |
| */ |
| private void removeReferences(@NotNull Node node) throws ReferentialIntegrityException, RepositoryException { |
| Collection<String> removableReferencePaths = new ArrayList<>(); |
| PropertyIterator pIter = node.getReferences(); |
| while (pIter.hasNext()) { |
| Property referenceProperty = pIter.nextProperty(); |
| if (isIncluded(referenceProperty, 0) || idConflictPolicy == IdConflictPolicy.FORCE_REMOVE_CONFLICTING_ID) { |
| removableReferencePaths.add(referenceProperty.getPath()); |
| } else { |
| throw new ReferentialIntegrityException("Found non-removable reference for conflicting UUID " + node.getIdentifier() + " (" + node.getPath() + ") at " + referenceProperty.getPath()); |
| } |
| } |
| for (String referencePath : removableReferencePaths) { |
| log.info("Remove reference towards {} at {}", node.getIdentifier(), referencePath); |
| session.removeItem(referencePath); |
| } |
| } |
| |
| private @Nullable Node updateExistingNode(@NotNull Node node, @NotNull DocViewNode2 ni, @NotNull ImportMode importMode) throws RepositoryException { |
| VersioningState vs = new VersioningState(stack, node); |
| Node updatedNode = null; |
| Optional<String> identifier = ni.getIdentifier(); |
| // try to set uuid via sysview import if it differs from existing one |
| if (identifier.isPresent() && !node.getIdentifier().equals(identifier.get()) && !"rep:root".equals(ni.getPrimaryType().orElse(""))) { |
| NodeStash stash = new NodeStash(session, node.getPath()); |
| stash.stash(importInfo); |
| Node parent = node.getParent(); |
| removeReferences(node); |
| node.remove(); |
| updatedNode = createNewNode(parent, ni); |
| stash.recover(importMode, importInfo); |
| } else { |
| // TODO: is this faster than using sysview import? |
| // set new primary type (but never set rep:root) |
| String newPrimaryType = ni.getPrimaryType().orElseThrow(() -> new IllegalStateException("Mandatory property 'jcr:primaryType' missing from " + ni)); |
| if (importMode == ImportMode.REPLACE && !"rep:root".equals(newPrimaryType) && wspFilter.includesProperty(PathUtil.append(node.getPath(), JcrConstants.JCR_PRIMARYTYPE))) { |
| String currentPrimaryType = node.getPrimaryNodeType().getName(); |
| if (!currentPrimaryType.equals(newPrimaryType)) { |
| vs.ensureCheckedOut(); |
| log.trace("Changing primary node type for {} from {} to {}", node.getPath(), currentPrimaryType, newPrimaryType); |
| node.setPrimaryType(newPrimaryType); |
| updatedNode = node; |
| } |
| } |
| // calculate mixins to be added |
| Set<String> newMixins = new HashSet<>(); |
| if (wspFilter.includesProperty(PathUtil.append(node.getPath(), JcrConstants.JCR_MIXINTYPES))) { |
| AccessControlHandling acHandling = getAcHandling(ni.getName()); |
| for (String mixin : ni.getMixinTypes()) { |
| // omit if mix:AccessControllable and CLEAR |
| if (!aclManagement.isAccessControllableMixin(mixin) |
| || acHandling != AccessControlHandling.CLEAR) { |
| newMixins.add(mixin); |
| } |
| } |
| // remove mixins not in package (only for mode = replace) |
| if (importMode == ImportMode.REPLACE) { |
| for (NodeType mix : node.getMixinNodeTypes()) { |
| String name = mix.getName(); |
| if (!newMixins.remove(name)) { |
| // special check for mix:AccessControllable |
| if (!aclManagement.isAccessControllableMixin(name) |
| || acHandling == AccessControlHandling.CLEAR |
| || acHandling == AccessControlHandling.OVERWRITE) { |
| vs.ensureCheckedOut(); |
| node.removeMixin(name); |
| updatedNode = node; |
| } |
| } |
| } |
| } |
| // add remaining mixins (for all import modes) |
| for (String mixin : newMixins) { |
| vs.ensureCheckedOut(); |
| node.addMixin(mixin); |
| updatedNode = node; |
| } |
| } |
| // remove unprotected properties not in package (only for mode = replace) |
| if (importMode == ImportMode.REPLACE) { |
| PropertyIterator pIter = node.getProperties(); |
| while (pIter.hasNext()) { |
| Property p = pIter.nextProperty(); |
| String propName = p.getName(); |
| if (!p.getDefinition().isProtected() |
| && !ni.hasProperty(npResolver.getQName(propName)) |
| && !preserveProperties.contains(p.getPath()) |
| && wspFilter.includesProperty(p.getPath())) { |
| vs.ensureCheckedOut(); |
| p.remove(); |
| updatedNode = node; |
| } |
| } |
| } |
| EffectiveNodeType effectiveNodeType = EffectiveNodeType.ofNode(node); |
| // logging for uncovered protected properties |
| logIgnoredProtectedProperties(effectiveNodeType, node.getPath(), ni.getProperties(), PROTECTED_PROPERTIES_CONSIDERED_FOR_UPDATED_NODES); |
| |
| // add/modify properties contained in package |
| if (setUnprotectedProperties(effectiveNodeType, node, ni, importMode == ImportMode.REPLACE|| importMode == ImportMode.UPDATE || importMode == ImportMode.UPDATE_PROPERTIES, vs)) { |
| updatedNode = node; |
| } |
| } |
| return updatedNode; |
| } |
| |
| /** |
| * Creates a new node via system view XML and {@link Session#importXML(String, InputStream, int)} to be able to set protected properties. |
| * Afterwards uses regular JCR API to set unprotected properties (on a best-effort basis as this depends on the repo implementation). |
| * @param parentNode the parent node below which the new node should be created |
| * @param ni the information about the new node to be created |
| * @return the newly created node |
| * @throws RepositoryException |
| */ |
| private @NotNull Node createNewNode(Node parentNode, DocViewNode2 ni) |
| throws RepositoryException { |
| final int importUuidBehavior; |
| switch(idConflictPolicy) { |
| case CREATE_NEW_ID: |
| // what happens to references? |
| importUuidBehavior = ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW; |
| break; |
| case FORCE_REMOVE_CONFLICTING_ID: |
| importUuidBehavior = ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING; |
| break; |
| default: |
| importUuidBehavior = ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW; |
| break; |
| } |
| try { |
| String parentPath = parentNode.getPath(); |
| final ContentHandler handler = session.getImportContentHandler( |
| parentPath, |
| importUuidBehavior); |
| // first define the current namespaces |
| String[] prefixes = session.getNamespacePrefixes(); |
| handler.startDocument(); |
| for (String prefix : prefixes) { |
| handler.startPrefixMapping(prefix, session.getNamespaceURI(prefix)); |
| } |
| AttributesImpl attrs = new AttributesImpl(); |
| attrs.addAttribute(Name.NS_SV_URI, "name", "sv:name", ATTRIBUTE_TYPE_CDATA, npResolver.getJCRName(ni.getName())); |
| handler.startElement(Name.NS_SV_URI, "node", "sv:node", attrs); |
| |
| // check if SNS and a helper uuid if needed |
| boolean addMixRef = false; |
| |
| if (ni.getIndex() > 0 && !ni.getIdentifier().isPresent()) { |
| Collection<DocViewProperty2> preprocessedProperties = new LinkedList<>(ni.getProperties()); |
| preprocessedProperties.add(new DocViewProperty2( NameConstants.JCR_UUID, UUID.randomUUID().toString(), PropertyType.STRING)); |
| // check mixins |
| DocViewProperty2 mix = ni.getProperty(NameConstants.JCR_MIXINTYPES).orElse(null); |
| addMixRef = true; |
| if (mix == null) { |
| mix = new DocViewProperty2(NameConstants.JCR_MIXINTYPES, Collections.singletonList(JcrConstants.MIX_REFERENCEABLE), PropertyType.NAME); |
| preprocessedProperties.add(mix); |
| } else { |
| for (String v : mix.getStringValues()) { |
| if (v.equals(JcrConstants.MIX_REFERENCEABLE)) { |
| addMixRef = false; |
| break; |
| } |
| } |
| if (addMixRef) { |
| List<String> mixinValues = new LinkedList<>(mix.getStringValues()); |
| mixinValues.add(JcrConstants.MIX_REFERENCEABLE); |
| preprocessedProperties.remove(mix); |
| mix = new DocViewProperty2(NameConstants.JCR_MIXINTYPES, mixinValues, PropertyType.NAME); |
| preprocessedProperties.add(mix); |
| } |
| } |
| ni = ni.cloneWithDifferentProperties(preprocessedProperties); |
| } |
| |
| String nodePath = PathUtil.append(parentPath, npResolver.getJCRName(ni.getName())); |
| // add the protected properties |
| for (DocViewProperty2 p : ni.getProperties()) { |
| String qualifiedPropertyName = npResolver.getJCRName(p.getName()); |
| if (p.getStringValue().isPresent() && PROTECTED_PROPERTIES_CONSIDERED_FOR_NEW_NODES.contains(p.getName()) && wspFilter.includesProperty(nodePath + "/" + qualifiedPropertyName)) { |
| attrs = new AttributesImpl(); |
| attrs.addAttribute(Name.NS_SV_URI, "name", "sv:name", ATTRIBUTE_TYPE_CDATA, qualifiedPropertyName); |
| attrs.addAttribute(Name.NS_SV_URI, "type", "sv:type", ATTRIBUTE_TYPE_CDATA, PropertyType.nameFromValue(p.getType())); |
| handler.startElement(Name.NS_SV_URI, "property", "sv:property", attrs); |
| for (String v : p.getStringValues()) { |
| handler.startElement(Name.NS_SV_URI, "value", "sv:value", DocViewSAXHandler.EMPTY_ATTRIBUTES); |
| handler.characters(v.toCharArray(), 0, v.length()); |
| handler.endElement(Name.NS_SV_URI, "value", "sv:value"); |
| } |
| handler.endElement(Name.NS_SV_URI, "property", "sv:property"); |
| } |
| } |
| handler.endElement(Name.NS_SV_URI, "node", "sv:node"); |
| handler.endDocument(); |
| |
| // retrieve newly created node either by uuid, label or name |
| Node node = getNodeByIdOrName(parentNode, ni, importUuidBehavior == ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); |
| EffectiveNodeType effectiveNodeType = EffectiveNodeType.ofNode(node); |
| |
| // logging for uncovered protected properties |
| logIgnoredProtectedProperties(effectiveNodeType, node.getPath(), ni.getProperties(), PROTECTED_PROPERTIES_CONSIDERED_FOR_NEW_NODES); |
| setUnprotectedProperties(effectiveNodeType, node, ni, true, null); |
| // remove mix referenceable if it was temporarily added |
| if (addMixRef) { |
| node.removeMixin(JcrConstants.MIX_REFERENCEABLE); |
| } |
| return node; |
| |
| } catch (SAXException e) { |
| Exception root = e.getException(); |
| if (root instanceof RepositoryException) { |
| if (root instanceof ConstraintViolationException) { |
| // potentially rollback changes in the transient space (only relevant for Oak, https://issues.apache.org/jira/browse/OAK-9436), as otherwise the same exception is thrown again at Session.save() |
| try { |
| Node node = getNodeByIdOrName(parentNode, ni, importUuidBehavior == ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); |
| node.remove(); |
| } catch (RepositoryException re) { |
| // ignore as no node found when the transient space is clean already |
| } |
| } |
| throw (RepositoryException) root; |
| } else if (root instanceof RuntimeException) { |
| throw (RuntimeException) root; |
| } else { |
| throw new RepositoryException("Error while creating node", root); |
| } |
| } |
| } |
| |
| private void logIgnoredProtectedProperties(EffectiveNodeType effectiveNodeType, String nodePath, Collection<DocViewProperty2> properties, Set<Name> importedProtectedProperties) { |
| // logging for protected properties which are not considered during import |
| properties.stream() |
| .filter(p -> p.getStringValue().isPresent() |
| && !importedProtectedProperties.contains(p.getName())) |
| .forEach(p -> { |
| try { |
| if (isPropertyProtected(effectiveNodeType, p)) { |
| log.warn("Ignore protected property '{}' on node '{}'", npResolver.getJCRName(p.getName()), nodePath); |
| } |
| } catch (RepositoryException e) { |
| throw new IllegalStateException("Error retrieving protected status of properties", e); |
| } |
| }); |
| } |
| |
| /** |
| * Determines if a given property is protected according to the node type. |
| * |
| * @param effectiveNodeType the effective node type |
| * @param docViewProperty the property |
| * @return{@code true} in case the property is protected, {@code false} otherwise |
| * @throws RepositoryException |
| */ |
| private boolean isPropertyProtected(@NotNull EffectiveNodeType effectiveNodeType, @NotNull DocViewProperty2 docViewProperty) throws RepositoryException { |
| return effectiveNodeType.getApplicablePropertyDefinition(npResolver.getJCRName(docViewProperty.getName()), docViewProperty.isMultiValue(), docViewProperty.getType()).map(PropertyDefinition::isProtected).orElse(false); |
| } |
| |
| private Node getNodeByIdOrName(@NotNull Node currentNode, @NotNull DocViewNode2 ni, boolean isIdNewlyAssigned) throws RepositoryException { |
| Node node = null; |
| Optional<String> id = ni.getIdentifier(); |
| String name = npResolver.getJCRName(ni.getName()); |
| if (id.isPresent() && !isIdNewlyAssigned) { |
| try { |
| node = currentNode.getSession().getNodeByIdentifier(id.get()); |
| } catch (RepositoryException e) { |
| log.warn("Newly created node not found by uuid {}: {}", currentNode.getPath() + "/" + name, e.toString()); |
| } |
| } |
| if (node == null) { |
| String snsName = npResolver.getJCRName(ni.getSnsAwareName()); |
| try { |
| node = currentNode.getNode(snsName); |
| } catch (RepositoryException e) { |
| log.warn("Newly created node not found by SNS aware name {}: {}", currentNode.getPath() + "/" + snsName, e.toString()); |
| } |
| } |
| if (node == null) { |
| try { |
| node = currentNode.getNode(name); |
| } catch (RepositoryException e) { |
| log.debug("Newly created node not found by name {}: {}", currentNode.getPath() + "/" + name, e.toString()); |
| throw e; |
| } |
| } |
| return node; |
| } |
| |
| private boolean setUnprotectedProperties(@NotNull EffectiveNodeType effectiveNodeType, @NotNull Node node, @NotNull DocViewNode2 ni, boolean overwriteExistingProperties, @Nullable VersioningState vs) throws RepositoryException { |
| boolean isAtomicCounter = false; |
| for (String mixin : ni.getMixinTypes()) { |
| if ("mix:atomicCounter".equals(mixin)) { |
| isAtomicCounter = true; |
| } |
| } |
| |
| boolean modified = false; |
| // add properties |
| for (DocViewProperty2 prop : ni.getProperties()) { |
| String name = npResolver.getJCRName(prop.getName()); |
| if (prop != null && !isPropertyProtected(effectiveNodeType, prop) && (overwriteExistingProperties || !node.hasProperty(name)) && wspFilter.includesProperty(node.getPath() + "/" + npResolver.getJCRName(prop.getName()))) { |
| // check if property is allowed |
| try { |
| modified |= prop.apply(node); |
| } catch (RepositoryException e) { |
| try { |
| if (vs == null) { |
| throw e; |
| } |
| // try again with checked out node |
| vs.ensureCheckedOut(); |
| modified |= prop.apply(node); |
| } catch (RepositoryException e1) { |
| // be lenient in case of mode != replace |
| if (wspFilter.getImportMode(node.getPath()) != ImportMode.REPLACE) { |
| log.warn("Error while setting property {} (ignore due to mode {}): {}", prop.getName(), wspFilter.getImportMode(node.getPath()), e1); |
| } else { |
| throw e; |
| } |
| } |
| } |
| } |
| } |
| |
| // adjust oak atomic counter |
| if (isAtomicCounter && wspFilter.includesProperty(node.getPath() + "/" + npResolver.getJCRName(NAME_OAK_COUNTER))) { |
| long previous = 0; |
| if (node.hasProperty(NAME_OAK_COUNTER.toString())) { |
| previous = node.getProperty(NAME_OAK_COUNTER.toString()).getLong(); |
| } |
| long counter = 0; |
| try { |
| counter = ni.getPropertyValue(NAME_OAK_COUNTER).map(Long::valueOf).orElse(0L); |
| } catch (NumberFormatException e) { |
| // ignore |
| } |
| if (counter != previous) { |
| node.setProperty("oak:increment", counter - previous); |
| modified = true; |
| } |
| } |
| return modified; |
| } |
| |
| /** |
| * Returns proper access control handling value based on the node |
| * name. |
| * @param nodeName name of the access control node |
| * @return cugHandling for CUG related nodes, aclHandling for |
| * everything else |
| */ |
| @NotNull |
| private AccessControlHandling getAcHandling(@NotNull Name nodeName) { |
| if (cugHandling != null && NAME_REP_CUG_POLICY.equals(nodeName)) { |
| return cugHandling; |
| } else { |
| return aclHandling; |
| } |
| } |
| |
| |
| /** |
| * Encapsulates information about the node which has been imported last |
| * TODO: minimize as some stack is managed by the oarser already (i.e. the full jcr paths) |
| */ |
| private class StackElement { |
| |
| private final Node node; |
| |
| private StackElement parent; |
| |
| private final NodeNameList childNames = new NodeNameList(); |
| |
| private boolean isCheckedOut; |
| |
| private boolean isNew; |
| |
| /** |
| * adapter for special content |
| */ |
| private DocViewAdapter adapter; |
| |
| public StackElement(@Nullable Node node, boolean isNew) throws RepositoryException { |
| this.node = node; |
| this.isNew = isNew; |
| isCheckedOut = node == null || !node.isNodeType(JcrConstants.MIX_VERSIONABLE) || node.isCheckedOut(); |
| } |
| |
| public Node getNode() { |
| return node; |
| } |
| |
| public boolean isCheckedOut() { |
| return isCheckedOut && (parent == null || parent.isCheckedOut()); |
| } |
| |
| public void ensureCheckedOut() throws RepositoryException { |
| if (!isCheckedOut) { |
| importInfo.registerToVersion(node.getPath()); |
| try { |
| node.checkout(); |
| } catch (RepositoryException e) { |
| log.warn("error while checkout node (ignored)", e); |
| } |
| isCheckedOut = true; |
| } |
| if (parent != null) { |
| parent.ensureCheckedOut(); |
| } |
| } |
| |
| public boolean isRoot() { |
| return parent == null; |
| } |
| |
| public boolean checkForNode() { |
| // we should check if child node exist if stack is not new or if it's a root node |
| return !isNew || parent == null; |
| } |
| |
| public void addName(Name name) throws NamespaceException { |
| childNames.addName(npResolver.getJCRName(name)); |
| } |
| |
| public NodeNameList getChildNames() { |
| return childNames; |
| } |
| |
| public void restoreOrder() throws RepositoryException { |
| if (checkForNode() && childNames.needsReorder(node)) { |
| ensureCheckedOut(); |
| childNames.restoreOrder(node); |
| } |
| } |
| |
| public StackElement push() throws RepositoryException { |
| return push(new StackElement(null, false)); |
| } |
| |
| public StackElement push(StackElement elem) throws RepositoryException { |
| elem.parent = this; |
| return elem; |
| } |
| |
| |
| public StackElement pop() { |
| return parent; |
| } |
| |
| public DocViewAdapter getAdapter() { |
| if (adapter != null) { |
| return adapter; |
| } |
| return parent == null ? null : parent.getAdapter(); |
| } |
| |
| } |
| |
| /** |
| * Helper class that stores information about attachments |
| */ |
| private static class BlobInfo { |
| |
| private final boolean isMulti; |
| |
| private final List<Artifact> artifacts = new ArrayList<>(); |
| |
| public BlobInfo(boolean multi) { |
| isMulti = multi; |
| } |
| |
| public boolean isFile() { |
| return !artifacts.isEmpty() && artifacts.get(0).getType() == ArtifactType.FILE; |
| } |
| |
| public void add(Artifact a) { |
| assert artifacts.isEmpty(); |
| artifacts.add(a); |
| } |
| |
| public void add(int idx, Artifact a) { |
| while (idx >= artifacts.size()) { |
| artifacts.add(null); |
| } |
| artifacts.set(idx, a); |
| } |
| |
| public Value[] getValues(Session session) |
| throws RepositoryException, IOException { |
| Value[] values = new Value[artifacts.size()]; |
| for (int i = 0; i < values.length; i++) { |
| Artifact a = artifacts.get(i); |
| try (InputStream input = a.getInputStream()) { |
| values[i] = session.getValueFactory().createValue(input); |
| } |
| } |
| return values; |
| } |
| |
| public Value getValue(Session session) |
| throws RepositoryException, IOException { |
| Artifact a = artifacts.get(0); |
| try (InputStream input = a.getInputStream()) { |
| return session.getValueFactory().createValue(input); |
| } |
| } |
| |
| public void detach() { |
| for (Artifact a : artifacts) { |
| if (a instanceof PropertyValueArtifact) { |
| try { |
| ((PropertyValueArtifact) a).detach(); |
| } catch (IOException|RepositoryException e) { |
| log.warn("error while detaching property artifact", e); |
| } |
| } |
| } |
| } |
| } |
| |
| private class VersioningState { |
| |
| private final StackElement stack; |
| |
| private final Node node; |
| |
| private boolean isCheckedOut; |
| |
| private boolean isParentCheckedOut; |
| |
| private VersioningState(StackElement stack, Node node) throws RepositoryException { |
| this.stack = stack; |
| this.node = node; |
| isCheckedOut = node == null || !node.isNodeType(JcrConstants.MIX_VERSIONABLE) || node.isCheckedOut(); |
| isParentCheckedOut = stack.isCheckedOut(); |
| } |
| |
| public void ensureCheckedOut() throws RepositoryException { |
| if (!isCheckedOut) { |
| importInfo.registerToVersion(node.getPath()); |
| try { |
| node.checkout(); |
| } catch (RepositoryException e) { |
| log.warn("error while checkout node (ignored)", e); |
| } |
| isCheckedOut = true; |
| } |
| if (!isParentCheckedOut) { |
| stack.ensureCheckedOut(); |
| isParentCheckedOut = true; |
| } |
| } |
| } |
| |
| } |