/*
* Licensed to DuraSpace under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* DuraSpace 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.fcrepo.kernel.modeshape.utils;
import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static org.apache.jena.datatypes.xsd.XSDDatatype.XSDstring;
import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FIELD_DELIMITER;
import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getJcrNode;
import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getReferencePropertyName;
import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isExternalNode;
import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isInternalReferenceProperty;
import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isMultivaluedProperty;
import static org.fcrepo.kernel.modeshape.utils.UncheckedPredicate.uncheck;
import static org.slf4j.LoggerFactory.getLogger;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.lang3.StringUtils;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import org.apache.jena.rdf.model.Resource;
import org.fcrepo.kernel.api.models.FedoraResource;
import org.fcrepo.kernel.api.exception.IdentifierConversionException;
import org.fcrepo.kernel.api.exception.NoSuchPropertyDefinitionException;
import org.fcrepo.kernel.api.identifiers.IdentifierConverter;
import org.slf4j.Logger;
/**
* Tools for replacing, appending and deleting JCR node properties
* @author Chris Beer
* @author ajs6f
* @since May 10, 2013
*/
public class NodePropertiesTools {
private static final Logger LOGGER = getLogger(NodePropertiesTools.class);
/**
* Given a JCR node, property and value, either:
* - if the property is single-valued, replace the existing property with
* the new value
* - if the property is multivalued, append the new value to the property
* @param node the JCR node
* @param propertyName a name of a JCR property (either pre-existing or
* otherwise)
* @param newValue the JCR value to insert
* @throws RepositoryException if repository exception occurred
*/
public void appendOrReplaceNodeProperty(final Node node, final String propertyName, final Value newValue)
throws RepositoryException {
final Property property;
// if it already exists, we can take some shortcuts
if (node.hasProperty(propertyName)) {
property = node.getProperty(propertyName);
if (property.isMultiple()) {
LOGGER.debug("Appending value {} to {} property {}", newValue,
PropertyType.nameFromValue(property.getType()),
propertyName);
// if the property is multi-valued, go ahead and append to it.
final List<Value> newValues = new ArrayList<>(asList(node.getProperty(propertyName).getValues()));
if (!newValues.contains(newValue)) {
newValues.add(newValue);
property.setValue(newValues.toArray(new Value[newValues.size()]));
}
} else {
// or we'll just overwrite its single value
LOGGER.debug("Overwriting {} property {} with new value {}", PropertyType.nameFromValue(property
.getType()), propertyName, newValue);
property.setValue(newValue);
}
} else {
// we're creating a new property on this node, so we check whether it should be multi-valued
boolean isMultiple = true;
try {
isMultiple = isMultivaluedProperty(node, propertyName);
} catch (final NoSuchPropertyDefinitionException e) {
// simply represents a new kind of property on this node
}
if (isMultiple) {
LOGGER.debug("Creating new multivalued {} property {} with " +
"initial value [{}]",
PropertyType.nameFromValue(newValue.getType()),
propertyName, newValue);
property = node.setProperty(propertyName, new Value[]{newValue}, newValue.getType());
} else {
LOGGER.debug("Creating new single-valued {} property {} with " +
"initial value {}",
PropertyType.nameFromValue(newValue.getType()),
propertyName, newValue);
property = node.setProperty(propertyName, newValue, newValue.getType());
}
}
if (!property.isMultiple() && !isInternalReferenceProperty.test(property)) {
final String referencePropertyName = getReferencePropertyName(propertyName);
if (node.hasProperty(referencePropertyName)) {
node.getProperty(referencePropertyName).remove();
}
}
}
/**
* Add a reference placeholder from one node to another in-domain resource
* @param idTranslator the id translator
* @param node the node
* @param propertyName the property name
* @param resource the resource
* @throws RepositoryException if repository exception occurred
*/
public void addReferencePlaceholders(final IdentifierConverter<Resource,FedoraResource> idTranslator,
final Node node,
final String propertyName,
final Resource resource) throws RepositoryException {
try {
final Node refNode = getJcrNode(idTranslator.convert(resource));
if (isExternalNode.test(refNode)) {
// we can't apply REFERENCE properties to external resources
return;
}
final String referencePropertyName = getReferencePropertyName(propertyName);
if (!isMultivaluedProperty(node, propertyName)) {
if (node.hasProperty(referencePropertyName)) {
node.getProperty(referencePropertyName).remove();
}
if (node.hasProperty(propertyName)) {
node.getProperty(propertyName).remove();
}
}
final Value v = node.getSession().getValueFactory().createValue(refNode, true);
appendOrReplaceNodeProperty(node, referencePropertyName, v);
} catch (final IdentifierConversionException e) {
// no-op
}
}
/**
* Remove a reference placeholder that links one node to another in-domain resource
* @param idTranslator the id translator
* @param node the node
* @param propertyName the property name
* @param resource the resource
* @throws RepositoryException if repository exception occurred
*/
public void removeReferencePlaceholders(final IdentifierConverter<Resource,FedoraResource> idTranslator,
final Node node,
final String propertyName,
final Resource resource) throws RepositoryException {
final String referencePropertyName = getReferencePropertyName(propertyName);
final Node refNode = getJcrNode(idTranslator.convert(resource));
final Value v = node.getSession().getValueFactory().createValue(refNode, true);
removeNodeProperty(node, referencePropertyName, v);
}
/**
* Given a JCR node, property and value, remove the value (if it exists)
* from the property, and remove the
* property if no values remove
*
* @param node the JCR node
* @param propertyName a name of a JCR property (either pre-existing or
* otherwise)
* @param valueToRemove the JCR value to remove
* @throws RepositoryException if repository exception occurred
*/
public void removeNodeProperty(final Node node, final String propertyName, final Value valueToRemove)
throws RepositoryException {
LOGGER.debug("Request to remove {}", valueToRemove);
// if the property doesn't exist, we don't need to worry about it.
if (node.hasProperty(propertyName)) {
final Property property = node.getProperty(propertyName);
final String strValueToRemove = valueToRemove.getString();
final String strValueToRemoveWithoutStringType = strValueToRemove != null ?
strValueToRemove.replace(FIELD_DELIMITER + XSDstring.getURI(), "") : strValueToRemove;
if (property.isMultiple()) {
final AtomicBoolean remove = new AtomicBoolean();
final Value[] newValues = stream(node.getProperty(propertyName).getValues()).filter(uncheck(v -> {
final String strVal = v.getString().replace(FIELD_DELIMITER + XSDstring.getURI(), "");
LOGGER.debug("v is '{}', valueToRemove is '{}'", v, strValueToRemove );
if (strVal.equals(strValueToRemoveWithoutStringType)) {
remove.set(true);
return false;
}
return true;
})).toArray(Value[]::new);
// we only need to update the property if we did anything.
if (remove.get()) {
if (newValues.length == 0) {
LOGGER.debug("Removing property '{}'", propertyName);
property.remove();
} else {
LOGGER.debug("Removing value '{}' from property '{}'", strValueToRemove, propertyName);
property.setValue(newValues);
}
} else {
LOGGER.debug("Value not removed from property name '{}' (value '{}')", propertyName,
strValueToRemove);
}
} else {
final String strPropVal = property.getValue().getString();
final String strPropValWithoutStringType =
strPropVal != null ? strPropVal.replace(FIELD_DELIMITER + XSDstring.getURI(), "") : strPropVal;
LOGGER.debug("Removing string '{}'", strValueToRemove);
if (StringUtils.equals(strPropValWithoutStringType, strValueToRemoveWithoutStringType)) {
LOGGER.debug("single value: Removing value from property '{}'", propertyName);
property.remove();
} else {
LOGGER.debug("Value not removed from property name '{}' (property value: '{}';compare value: '{}')",
propertyName, strPropVal, strValueToRemove);
throw new RepositoryException("Property '" + propertyName + "': Unable to remove value '" +
StringUtils.substring(strValueToRemove, 0, 50) + "'");
}
}
}
}
}