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}