001/* 002 * Licensed to DuraSpace under one or more contributor license agreements. 003 * See the NOTICE file distributed with this work for additional information 004 * regarding copyright ownership. 005 * 006 * DuraSpace licenses this file to you under the Apache License, 007 * Version 2.0 (the "License"); you may not use this file except in 008 * compliance with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.fcrepo.kernel.modeshape.utils; 019 020import static java.util.Arrays.asList; 021import static java.util.Arrays.stream; 022import static org.apache.jena.datatypes.xsd.XSDDatatype.XSDstring; 023import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FIELD_DELIMITER; 024import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getJcrNode; 025import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getReferencePropertyName; 026import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isExternalNode; 027import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isInternalReferenceProperty; 028import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isMultivaluedProperty; 029import static org.fcrepo.kernel.modeshape.utils.UncheckedPredicate.uncheck; 030import static org.slf4j.LoggerFactory.getLogger; 031 032import java.util.ArrayList; 033import java.util.List; 034import java.util.concurrent.atomic.AtomicBoolean; 035 036import org.apache.commons.lang3.StringUtils; 037 038import javax.jcr.Node; 039import javax.jcr.Property; 040import javax.jcr.PropertyType; 041import javax.jcr.RepositoryException; 042import javax.jcr.Value; 043 044import org.apache.jena.rdf.model.Resource; 045 046import org.apache.jena.vocabulary.RDF; 047import org.fcrepo.kernel.api.models.FedoraResource; 048import org.fcrepo.kernel.api.exception.IdentifierConversionException; 049import org.fcrepo.kernel.api.exception.NoSuchPropertyDefinitionException; 050import org.fcrepo.kernel.api.identifiers.IdentifierConverter; 051import org.slf4j.Logger; 052 053/** 054 * Tools for replacing, appending and deleting JCR node properties 055 * @author Chris Beer 056 * @author ajs6f 057 * @since May 10, 2013 058 */ 059public class NodePropertiesTools { 060 061 private static final Logger LOGGER = getLogger(NodePropertiesTools.class); 062 063 /** 064 * Given a JCR node, property and value, either: 065 * - if the property is single-valued, replace the existing property with 066 * the new value 067 * - if the property is multivalued, append the new value to the property 068 * @param node the JCR node 069 * @param propertyName a name of a JCR property (either pre-existing or 070 * otherwise) 071 * @param newValue the JCR value to insert 072 * @throws RepositoryException if repository exception occurred 073 */ 074 public void appendOrReplaceNodeProperty(final Node node, final String propertyName, final Value newValue) 075 throws RepositoryException { 076 077 final Property property; 078 079 // if it already exists, we can take some shortcuts 080 if (node.hasProperty(propertyName)) { 081 082 property = node.getProperty(propertyName); 083 084 if (property.isMultiple()) { 085 LOGGER.debug("Appending value {} to {} property {}", newValue, 086 PropertyType.nameFromValue(property.getType()), 087 propertyName); 088 089 // if the property is multi-valued, go ahead and append to it. 090 final List<Value> newValues = new ArrayList<>(asList(node.getProperty(propertyName).getValues())); 091 092 if (!newValues.contains(newValue)) { 093 newValues.add(newValue); 094 property.setValue(newValues.toArray(new Value[newValues.size()])); 095 } 096 } else { 097 // or we'll just overwrite its single value 098 LOGGER.debug("Overwriting {} property {} with new value {}", PropertyType.nameFromValue(property 099 .getType()), propertyName, newValue); 100 property.setValue(newValue); 101 } 102 } else { 103 // we're creating a new property on this node, so we check whether it should be multi-valued 104 boolean isMultiple = true; 105 try { 106 isMultiple = isMultivaluedProperty(node, propertyName); 107 } catch (final NoSuchPropertyDefinitionException e) { 108 // simply represents a new kind of property on this node 109 } 110 if (isMultiple) { 111 LOGGER.debug("Creating new multivalued {} property {} with " + 112 "initial value [{}]", 113 PropertyType.nameFromValue(newValue.getType()), 114 propertyName, newValue); 115 property = node.setProperty(propertyName, new Value[]{newValue}, newValue.getType()); 116 } else { 117 LOGGER.debug("Creating new single-valued {} property {} with " + 118 "initial value {}", 119 PropertyType.nameFromValue(newValue.getType()), 120 propertyName, newValue); 121 property = node.setProperty(propertyName, newValue, newValue.getType()); 122 } 123 } 124 125 if (!property.isMultiple() && !isInternalReferenceProperty.test(property)) { 126 final String referencePropertyName = getReferencePropertyName(propertyName); 127 if (node.hasProperty(referencePropertyName)) { 128 node.getProperty(referencePropertyName).remove(); 129 } 130 } 131 } 132 133 /** 134 * Add a reference placeholder from one node to another in-domain resource 135 * @param idTranslator the id translator 136 * @param node the node 137 * @param propertyName the property name 138 * @param resource the resource 139 * @throws RepositoryException if repository exception occurred 140 */ 141 public void addReferencePlaceholders(final IdentifierConverter<Resource,FedoraResource> idTranslator, 142 final Node node, 143 final String propertyName, 144 final Resource resource) throws RepositoryException { 145 146 try { 147 final Node refNode = getJcrNode(idTranslator.convert(resource)); 148 149 if (isExternalNode.test(refNode)) { 150 // we can't apply REFERENCE properties to external resources 151 return; 152 } 153 154 final String referencePropertyName = getReferencePropertyName(propertyName); 155 156 if (!isMultivaluedProperty(node, propertyName)) { 157 if (node.hasProperty(referencePropertyName)) { 158 node.getProperty(referencePropertyName).remove(); 159 } 160 161 if (node.hasProperty(propertyName)) { 162 node.getProperty(propertyName).remove(); 163 } 164 } 165 166 final Value v = node.getSession().getValueFactory().createValue(refNode, true); 167 appendOrReplaceNodeProperty(node, referencePropertyName, v); 168 169 } catch (final IdentifierConversionException e) { 170 // no-op 171 } 172 } 173 174 /** 175 * Remove a reference placeholder that links one node to another in-domain resource 176 * @param idTranslator the id translator 177 * @param node the node 178 * @param propertyName the property name 179 * @param resource the resource 180 * @throws RepositoryException if repository exception occurred 181 */ 182 public void removeReferencePlaceholders(final IdentifierConverter<Resource,FedoraResource> idTranslator, 183 final Node node, 184 final String propertyName, 185 final Resource resource) throws RepositoryException { 186 187 final String referencePropertyName = getReferencePropertyName(propertyName); 188 189 final Node refNode = getJcrNode(idTranslator.convert(resource)); 190 final Value v = node.getSession().getValueFactory().createValue(refNode, true); 191 removeNodeProperty(node, referencePropertyName, v); 192 } 193 /** 194 * Given a JCR node, property and value, remove the value (if it exists) 195 * from the property, and remove the 196 * property if no values remove 197 * 198 * @param node the JCR node 199 * @param propertyName a name of a JCR property (either pre-existing or 200 * otherwise) 201 * @param valueToRemove the JCR value to remove 202 * @throws RepositoryException if repository exception occurred 203 */ 204 public void removeNodeProperty(final Node node, final String propertyName, final Value valueToRemove) 205 throws RepositoryException { 206 LOGGER.debug("Request to remove {}", valueToRemove); 207 // if the property doesn't exist, we don't need to worry about it. 208 if (node.hasProperty(propertyName)) { 209 210 final Property property = node.getProperty(propertyName); 211 final String strValueToRemove = valueToRemove.getString(); 212 final String strValueToRemoveWithoutStringType = removeStringTypes(strValueToRemove); 213 214 if (property.isMultiple()) { 215 final AtomicBoolean remove = new AtomicBoolean(); 216 final Value[] newValues = stream(node.getProperty(propertyName).getValues()).filter(uncheck(v -> { 217 final String strVal = removeStringTypes(v.getString()); 218 219 LOGGER.debug("v is '{}', valueToRemove is '{}'", v, strValueToRemove ); 220 if (strVal.equals(strValueToRemoveWithoutStringType)) { 221 remove.set(true); 222 return false; 223 } 224 225 return true; 226 })).toArray(Value[]::new); 227 228 // we only need to update the property if we did anything. 229 if (remove.get()) { 230 if (newValues.length == 0) { 231 LOGGER.debug("Removing property '{}'", propertyName); 232 property.remove(); 233 } else { 234 LOGGER.debug("Removing value '{}' from property '{}'", strValueToRemove, propertyName); 235 property.setValue(newValues); 236 } 237 } else { 238 LOGGER.debug("Value not removed from property name '{}' (value '{}')", propertyName, 239 strValueToRemove); 240 } 241 } else { 242 243 final String strPropVal = property.getValue().getString(); 244 final String strPropValWithoutStringType = removeStringTypes(strPropVal); 245 246 LOGGER.debug("Removing string '{}'", strValueToRemove); 247 if (StringUtils.equals(strPropValWithoutStringType, strValueToRemoveWithoutStringType)) { 248 LOGGER.debug("single value: Removing value from property '{}'", propertyName); 249 property.remove(); 250 } else { 251 LOGGER.debug("Value not removed from property name '{}' (property value: '{}';compare value: '{}')", 252 propertyName, strPropVal, strValueToRemove); 253 throw new RepositoryException("Property '" + propertyName + "': Unable to remove value '" + 254 StringUtils.substring(strValueToRemove, 0, 50) + "'"); 255 } 256 } 257 } 258 } 259 260 private String removeStringTypes(final String value) { 261 if (value != null) { 262 // Remove string datatype 263 String v = value.replace(FIELD_DELIMITER + XSDstring.getURI(), ""); 264 265 // Remove lang datatype 266 v = v.replace(FIELD_DELIMITER + RDF.dtLangString.getURI(), FIELD_DELIMITER); 267 268 // Remove lang placeholder 269 return v.replace(FIELD_DELIMITER + FIELD_DELIMITER, ""); 270 } 271 return null; 272 } 273}