/* * 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.sling.ide.impl.vlt; import static org.apache.jackrabbit.vault.util.JcrConstants.JCR_CONTENT; import static org.apache.jackrabbit.vault.util.JcrConstants.JCR_DATA; import static org.apache.jackrabbit.vault.util.JcrConstants.JCR_LASTMODIFIED; import static org.apache.jackrabbit.vault.util.JcrConstants.JCR_PRIMARYTYPE; import static org.apache.jackrabbit.vault.util.JcrConstants.NT_RESOURCE; import static org.apache.sling.ide.transport.Repository.NT_FILE; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import javax.jcr.Binary; import javax.jcr.Credentials; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.PropertyType; import javax.jcr.Repository; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.Value; import javax.jcr.ValueFactory; import javax.jcr.nodetype.NodeType; import javax.jcr.nodetype.PropertyDefinition; import org.apache.jackrabbit.vault.util.JcrConstants; import org.apache.jackrabbit.vault.util.Text; import org.apache.sling.ide.filter.FilterResult; import org.apache.sling.ide.log.Logger; import org.apache.sling.ide.transport.CommandContext; import org.apache.sling.ide.transport.FileInfo; import org.apache.sling.ide.transport.Repository.CommandExecutionFlag; import org.apache.sling.ide.transport.ResourceProxy; import org.apache.sling.ide.util.PathUtil; public class AddOrUpdateNodeCommand extends JcrCommand<Void> { private ResourceProxy resource; private FileInfo fileInfo; private CommandContext context; public AddOrUpdateNodeCommand(Repository jcrRepo, Credentials credentials, CommandContext context, FileInfo fileInfo, ResourceProxy resource, Logger logger, CommandExecutionFlag... flags) { super(jcrRepo, credentials, resource.getPath(), logger, flags); this.context = context; this.fileInfo = fileInfo; this.resource = resource; } @Override protected Void execute0(Session session) throws RepositoryException, IOException { update(resource, session); return null; } private void update(ResourceProxy resource, Session session) throws RepositoryException, IOException { String path = resource.getPath(); boolean nodeExists = session.nodeExists(path); Node node; if (nodeExists) { node = session.getNode(path); getLogger().trace("Found existing node at {0} with primaryType {1}", path, node.getPrimaryNodeType().getName()); } else { node = createNode(resource, session); getLogger().trace("Created node at {0} with primaryType {1}", path, node.getPrimaryNodeType().getName()); } if (nodeExists && getFlags().contains(CommandExecutionFlag.CREATE_ONLY_WHEN_MISSING)) { return; } updateNode(node, resource); processDeletedNodes(node, resource); for (ResourceProxy child : resource.getCoveredChildren()) { update(child, session); } } private void processDeletedNodes(Node node, ResourceProxy resource2) throws RepositoryException { // TODO - we probably don't support SNS here ( and in other places as well ) List<ResourceProxy> resourceChildren = resource2.getChildren(); if (resourceChildren.size() == 0) { getLogger() .trace("Resource at {0} has no children, skipping deleted nodes processing", resource2.getPath()); return; } Map<String, ResourceProxy> resourceChildrenPaths = new HashMap<>(resourceChildren.size()); for (ResourceProxy child : resourceChildren) { resourceChildrenPaths.put(child.getPath(), child); } for (NodeIterator it = node.getNodes(); it.hasNext();) { Node child = it.nextNode(); if (resourceChildrenPaths.containsKey(child.getPath())) { // only descend for reordering when the child node is covered ; otherwise we // don't have enough information if (resource2.covers(child.getPath())) { processDeletedNodes(child, resourceChildrenPaths.get(child.getPath())); } continue; } if ( context.filter() != null && context.filter(). filter(child.getPath()) == FilterResult.DENY ) { getLogger().trace("Not deleting node at {0} since it is not included in the filter", child.getPath()); continue; } getLogger() .trace("Deleting node {0} as it is no longer present in the local checkout", child.getPath()); child.remove(); } } private Node createNode(ResourceProxy resource, Session session) throws RepositoryException, FileNotFoundException { String parentLocation = Text.getRelativeParent(resource.getPath(), 1); if (parentLocation.isEmpty()) { parentLocation = "/"; } if (!session.nodeExists(parentLocation)) { throw new RepositoryException("No parent found at " + parentLocation + " ; it's needed to create node at " + resource.getPath()); } String primaryType = (String) resource.getProperties().get(JCR_PRIMARYTYPE); Node parent = session.getNode(parentLocation); String childName = PathUtil.getName(resource.getPath()); if (primaryType == null) { return parent.addNode(childName); } else { return parent.addNode(childName, primaryType); } } private void updateNode(Node node, ResourceProxy resource) throws RepositoryException, IOException { if (node.getPath().equals(getPath()) && fileInfo != null) { updateFileLikeNodeTypes(node); } Set<String> propertiesToRemove = new HashSet<>(); PropertyIterator properties = node.getProperties(); while (properties.hasNext()) { Property property = properties.nextProperty(); if (property.getDefinition().isProtected() || property.getDefinition().isAutoCreated() || property.getDefinition().getRequiredType() == PropertyType.BINARY) { continue; } propertiesToRemove.add(property.getName()); } propertiesToRemove.removeAll(resource.getProperties().keySet()); Session session = node.getSession(); // update the mixin types ahead of type as contraints are enforced before // the session is committed Object mixinTypes = resource.getProperties().get(JcrConstants.JCR_MIXINTYPES); if (mixinTypes != null) { updateMixins(node, mixinTypes); } // remove old properties first // this supports the scenario where the node type is changed to a less permissive one for (String propertyToRemove : propertiesToRemove) { node.getProperty(propertyToRemove).remove(); getLogger().trace("Removed property {0} from node at {1}", propertyToRemove, node.getPath()); } String primaryType = (String) resource.getProperties().get(JcrConstants.JCR_PRIMARYTYPE); if (primaryType != null && !node.getPrimaryNodeType().getName().equals(primaryType) && node.getDepth() != 0) { node.setPrimaryType(primaryType); session.save(); getLogger().trace("Set new primary type {0} for node at {1}", primaryType, node.getPath()); } // TODO - review for completeness and filevault compatibility for (Map.Entry<String, Object> entry : resource.getProperties().entrySet()) { String propertyName = entry.getKey(); Object propertyValue = entry.getValue(); Property property = null; // it is possible that the property definition for 'jcr:mixinTypes' to not yet exist // so make sure that it does not get processed like a regular property if ( JcrConstants.JCR_MIXINTYPES.equals(propertyName) ) { continue; } if (node.hasProperty(propertyName)) { property = node.getProperty(propertyName); } if (property != null && property.getDefinition().isProtected()) { continue; } ValueFactory valueFactory = session.getValueFactory(); Value value = null; Value[] values = null; if (propertyValue instanceof String) { value = valueFactory.createValue((String) propertyValue); ensurePropertyDefinitionMatchers(property, PropertyType.STRING, false); } else if (propertyValue instanceof String[]) { values = toValueArray((String[]) propertyValue, session); ensurePropertyDefinitionMatchers(property, PropertyType.STRING, true); } else if (propertyValue instanceof Boolean) { value = valueFactory.createValue((Boolean) propertyValue); ensurePropertyDefinitionMatchers(property, PropertyType.BOOLEAN, false); } else if (propertyValue instanceof Boolean[]) { values = toValueArray((Boolean[]) propertyValue, session); ensurePropertyDefinitionMatchers(property, PropertyType.BOOLEAN, true); } else if (propertyValue instanceof Calendar) { value = valueFactory.createValue((Calendar) propertyValue); ensurePropertyDefinitionMatchers(property, PropertyType.DATE, false); } else if (propertyValue instanceof Calendar[]) { values = toValueArray((Calendar[]) propertyValue, session); ensurePropertyDefinitionMatchers(property, PropertyType.DATE, true); } else if (propertyValue instanceof Double) { value = valueFactory.createValue((Double) propertyValue); ensurePropertyDefinitionMatchers(property, PropertyType.DOUBLE, false); } else if (propertyValue instanceof Double[]) { values = toValueArray((Double[]) propertyValue, session); ensurePropertyDefinitionMatchers(property, PropertyType.DOUBLE, true); } else if (propertyValue instanceof BigDecimal) { value = valueFactory.createValue((BigDecimal) propertyValue); ensurePropertyDefinitionMatchers(property, PropertyType.DECIMAL, false); } else if (propertyValue instanceof BigDecimal[]) { values = toValueArray((BigDecimal[]) propertyValue, session); ensurePropertyDefinitionMatchers(property, PropertyType.DECIMAL, true); } else if (propertyValue instanceof Long) { value = valueFactory.createValue((Long) propertyValue); ensurePropertyDefinitionMatchers(property, PropertyType.LONG, false); } else if (propertyValue instanceof Long[]) { values = toValueArray((Long[]) propertyValue, session); ensurePropertyDefinitionMatchers(property, PropertyType.LONG, true); // TODO - distinguish between weak vs strong references } else if (propertyValue instanceof UUID) { Node reference = session.getNodeByIdentifier(((UUID) propertyValue).toString()); value = valueFactory.createValue(reference); ensurePropertyDefinitionMatchers(property, PropertyType.REFERENCE, false); } else if (propertyValue instanceof UUID[]) { values = toValueArray((UUID[]) propertyValue, session); ensurePropertyDefinitionMatchers(property, PropertyType.REFERENCE, true); } else { throw new IllegalArgumentException("Unable to handle value '" + propertyValue + "' for property '" + propertyName + "'"); } if (value != null) { Object[] arguments = { propertyName, value, propertyValue, node.getPath() }; getLogger().trace("Setting property {0} with value {1} (raw = {2}) on node at {3}", arguments); node.setProperty(propertyName, value); getLogger().trace("Set property {0} with value {1} (raw = {2}) on node at {3}", arguments); } else if (values != null) { Object[] arguments = { propertyName, values, propertyValue, node.getPath() }; getLogger().trace("Setting property {0} with values {1} (raw = {2}) on node at {3}", arguments); node.setProperty(propertyName, values); getLogger().trace("Set property {0} with values {1} (raw = {2}) on node at {3}", arguments); } else { throw new IllegalArgumentException("Unable to extract a value or a value array for property '" + propertyName + "' with value '" + propertyValue + "'"); } } } private void ensurePropertyDefinitionMatchers(Property property, int expectedType, boolean expectedMultiplicity) throws RepositoryException { if (property == null) { return; } PropertyDefinition definition = property.getDefinition(); if (definition.getRequiredType() != expectedType && definition.getRequiredType() != PropertyType.UNDEFINED) { getLogger().trace("Removing property {0} of type {1} since we need type {2}", property.getName(), definition.getRequiredType(), expectedType); property.remove(); return; } if (definition.isMultiple() != expectedMultiplicity) { getLogger().trace("Removing property {0} of multiplicity {1} since we need type {2}", property.getName(), definition.isMultiple(), expectedMultiplicity); property.remove(); return; } } private void updateMixins(Node node, Object mixinValue) throws RepositoryException { List<String> newMixins = new ArrayList<>(); if (mixinValue instanceof String) { newMixins.add((String) mixinValue); } else { newMixins.addAll(Arrays.asList((String[]) mixinValue)); } List<String> oldMixins = new ArrayList<>(); for (NodeType mixinNT : node.getMixinNodeTypes()) { oldMixins.add(mixinNT.getName()); } List<String> mixinsToAdd = new ArrayList<>(newMixins); mixinsToAdd.removeAll(oldMixins); List<String> mixinsToRemove = new ArrayList<>(oldMixins); mixinsToRemove.removeAll(newMixins); for (String mixinToAdd : mixinsToAdd) { node.addMixin(mixinToAdd); getLogger() .trace("Added new mixin {0} to node at path {1}", mixinToAdd, node.getPath()); } for (String mixinToRemove : mixinsToRemove) { node.removeMixin(mixinToRemove); getLogger() .trace("Removed mixin {0} from node at path {1}", mixinToRemove, node.getPath()); } } private void updateFileLikeNodeTypes(Node node) throws RepositoryException, IOException { // TODO - better handling of file-like nodes - perhaps we need to know the SerializationKind here // TODO - avoid IO File file = new File(fileInfo.getLocation()); if (!hasFileLikePrimaryNodeType(node)) { return; } Node contentNode; if (node.hasNode(JCR_CONTENT)) { contentNode = node.getNode(JCR_CONTENT); } else { if (node.getProperty(JCR_PRIMARYTYPE).getString().equals(NT_RESOURCE)) { contentNode = node; } else { contentNode = node.addNode(JCR_CONTENT, NT_RESOURCE); } } getLogger().trace("Updating {0} property on node at {1} ", JCR_DATA, contentNode.getPath()); try (FileInputStream inputStream = new FileInputStream(file)) { Binary binary = node.getSession().getValueFactory().createBinary(inputStream); contentNode.setProperty(JCR_DATA, binary); // TODO: might have to be done differently since the client and server's clocks can differ // and the last_modified should maybe be taken from the server's time.. contentNode.setProperty(JCR_LASTMODIFIED, Calendar.getInstance()); } } private boolean hasFileLikePrimaryNodeType(Node node) throws RepositoryException { return hasPrimaryNodeType(node, NT_FILE, NT_RESOURCE); } private boolean hasPrimaryNodeType(Node node, String... nodeTypeNames) throws RepositoryException { String primaryNodeTypeName = node.getPrimaryNodeType().getName(); for (String nodeTypeName : nodeTypeNames) { if (primaryNodeTypeName.equals(nodeTypeName)) { return true; } } for (NodeType supertype : node.getPrimaryNodeType().getSupertypes()) { String superTypeName = supertype.getName(); for (String nodeTypeName : nodeTypeNames) { if (superTypeName.equals(nodeTypeName)) { return true; } } } return false; } private Value[] toValueArray(String[] strings, Session session) throws RepositoryException { Value[] values = new Value[strings.length]; for (int i = 0; i < strings.length; i++) { values[i] = session.getValueFactory().createValue(strings[i]); } return values; } private Value[] toValueArray(Boolean[] booleans, Session session) throws RepositoryException { Value[] values = new Value[booleans.length]; for (int i = 0; i < booleans.length; i++) { values[i] = session.getValueFactory().createValue(booleans[i]); } return values; } private Value[] toValueArray(Calendar[] calendars, Session session) throws RepositoryException { Value[] values = new Value[calendars.length]; for (int i = 0; i < calendars.length; i++) { values[i] = session.getValueFactory().createValue(calendars[i]); } return values; } private Value[] toValueArray(Double[] doubles, Session session) throws RepositoryException { Value[] values = new Value[doubles.length]; for (int i = 0; i < doubles.length; i++) { values[i] = session.getValueFactory().createValue(doubles[i]); } return values; } private Value[] toValueArray(BigDecimal[] bigDecimals, Session session) throws RepositoryException { Value[] values = new Value[bigDecimals.length]; for (int i = 0; i < bigDecimals.length; i++) { values[i] = session.getValueFactory().createValue(bigDecimals[i]); } return values; } private Value[] toValueArray(Long[] longs, Session session) throws RepositoryException { Value[] values = new Value[longs.length]; for (int i = 0; i < longs.length; i++) { values[i] = session.getValueFactory().createValue(longs[i]); } return values; } private Value[] toValueArray(UUID[] uuids, Session session) throws RepositoryException { Value[] values = new Value[uuids.length]; for (int i = 0; i < uuids.length; i++) { Node reference = session.getNodeByIdentifier(uuids[i].toString()); values[i] = session.getValueFactory().createValue(reference); } return values; } @Override public Kind getKind() { return Kind.ADD_OR_UPDATE; } }