blob: a6d89af86babf65596f72e03e04443ac73ee85ff [file] [log] [blame]
/*
* 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.pipes.internal;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.pipes.BasePipe;
import org.apache.sling.pipes.PipeBindings;
import org.apache.sling.pipes.Plumber;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* pipe that writes to configured resource
*/
public class WritePipe extends BasePipe {
private static final Logger logger = LoggerFactory.getLogger(WritePipe.class);
public static final String RESOURCE_TYPE = RT_PREFIX + "write";
protected static final Pattern ADD_PATCH = Pattern.compile("\\+\\[(.*)\\]");
protected static final Pattern MULTI = Pattern.compile("\\[(.*)\\]");
Node confTree;
private List<Resource> propertiesToRemove;
/**
* public constructor
* @param plumber plumber instance
* @param resource configuration resource
* @param upperBindings super pipe's bindings
*/
public WritePipe(Plumber plumber, Resource resource, PipeBindings upperBindings) {
super(plumber, resource, upperBindings);
if (getConfiguration() == null){
String pathCandidate = getExpr();
Resource candidate = resolver.getResource(pathCandidate);
if (candidate != null) {
confTree = candidate.adaptTo(Node.class);
} else {
throw new IllegalArgumentException("write pipe is mis-configured: it should have a configuration node, or an expression");
}
} else {
confTree = getConfiguration().adaptTo(Node.class);
}
}
/**
* convert the configured string value (can be an expression) in a value that can be written in a resource.
* also handles patch for multivalue properties like <code>+[value]</code> in which case <code>value</code>
* is added to the MV property
* @param resource resource to which value will be written
* @param key property to which value will be written
* @param expression configured value to write
* @return actual value to write to the resource
*/
protected Object computeValue(Resource resource, String key, String expression) {
Object value = bindings.instantiateObject(expression);
if (value instanceof String) {
//in that case we treat special case like MV or patches
String sValue = (String)value;
Matcher patch = ADD_PATCH.matcher(sValue);
if (patch.matches()) {
String newValue = patch.group(1);
String[] actualValues = resource.getValueMap().get(key, String[].class);
List<String> newValues = actualValues != null ? new LinkedList<>(Arrays.asList(actualValues)) : new ArrayList<>();
if (!newValues.contains(newValue)) {
newValues.add(newValue);
}
return newValues.toArray(new String[newValues.size()]);
}
Matcher multiMatcher = MULTI.matcher(sValue);
if (multiMatcher.matches()) {
return multiMatcher.group(1).split(",");
}
}
return value;
}
/**
* convert the configured string value (can be an expression) in a value that can be written in a resource.
* also handles patch for multivalue properties like <code>+[value]</code> in which case <code>value</code>
* is added to the MV property
* @param resource resource to which value will be written
* @param key property to which value will be written
* @param expression configured value to write
* @return actual value to write to the resource
*/
protected Object computeValue(Resource resource, String key, Object expression) {
if (expression instanceof String) {
return computeValue(resource, key, (String)expression);
} else if (expression instanceof String[]){
List<String> values = new ArrayList<>();
for (String expr : (String[])expression){
values.add((String)computeValue(resource, key, expr));
}
return values.toArray(new String[values.size()]);
}
return expression;
}
@Override
public boolean modifiesContent() {
return true;
}
/**
* Write properties from the configuration to the target resource,
* instantiating both property names & values
*
* @param conf configured resource that holds all properties to write (and subpipes)
* @param target target resource on which configured values will be written
*/
private void copyProperties(@Nullable Resource conf, Resource target) {
ValueMap writeMap = conf != null ? conf.adaptTo(ValueMap.class) : null;
ModifiableValueMap properties = target.adaptTo(ModifiableValueMap.class);
//writing current node
if (properties != null && writeMap != null) {
for (Map.Entry<String, Object> entry : writeMap.entrySet()) {
if (!IGNORED_PROPERTIES.contains(entry.getKey())) {
String key = parent != null ? bindings.instantiateExpression(entry.getKey()) : entry.getKey();
Object value = computeValue(target, key, entry.getValue());
if (value == null) {
//null value are not handled by modifiable value maps,
//removing the property if it exists
addPropertyToRemove(target.getChild(key));
} else {
logger.info("writing {}@{}={}", target.getPath(), key, value);
if (!isDryRun()){
properties.put(key, value);
}
}
}
}
}
}
/**
* we store all property to remove for very last moment (in order to potentially reuse their value)
* @param property property resource that should be removed
*/
private void addPropertyToRemove(Resource property){
if (property != null) {
if (propertiesToRemove == null) {
propertiesToRemove = new ArrayList<>();
}
propertiesToRemove.add(property);
}
}
/**
* write the configured tree at the target resource, creating each node if needed, copying values.
* @param conf configuration JCR tree to write to target resource
* @param target target resource to write
*/
private void writeTree(Node conf, Resource target) throws RepositoryException {
copyProperties(resolver.getResource(conf.getPath()), target);
NodeIterator childrenConf = conf.getNodes();
if (childrenConf.hasNext()){
Node targetNode = target.adaptTo(Node.class);
if (targetNode != null) {
logger.info("dubbing {} at {}", conf.getPath(), target.getPath());
while (childrenConf.hasNext()){
Node childConf = childrenConf.nextNode();
String name = childConf.getName();
name = bindings.conditionalString(name);
if (name == null){
logger.debug("name has been instantiated as null, not writing that tree");
} else if (!isDryRun()){
Node childTarget = targetNode.hasNode(name) ? targetNode.getNode(name) : targetNode.addNode(name, childConf.getPrimaryNodeType().getName());
logger.debug("writing tree {}", childTarget.getPath());
writeTree(childConf, resolver.getResource(childTarget.getPath()));
}
}
}
}
}
@Override
protected Iterator<Resource> computeOutput() {
try {
Resource resource = getInput();
if (resource != null) {
writeTree(confTree, resource);
if (propertiesToRemove != null && !propertiesToRemove.isEmpty()){
for (Resource propertyResource : propertiesToRemove) {
logger.info("removing {}", propertyResource.getPath());
if (!isDryRun()){
Property property = propertyResource.adaptTo(Property.class);
if (property != null) {
property.remove();
}
}
}
}
return super.computeOutput();
}
} catch (RepositoryException e) {
throw new IllegalStateException(e);
} finally {
if (propertiesToRemove != null){
propertiesToRemove.clear();
}
}
return EMPTY_ITERATOR;
}
}