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.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getJcrNode;
023import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getReferencePropertyName;
024import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isExternalNode;
025import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isInternalReferenceProperty;
026import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isMultivaluedProperty;
027import static org.fcrepo.kernel.modeshape.utils.UncheckedPredicate.uncheck;
028import static org.slf4j.LoggerFactory.getLogger;
029
030import java.util.ArrayList;
031import java.util.List;
032import java.util.concurrent.atomic.AtomicBoolean;
033
034import org.apache.commons.lang3.StringUtils;
035
036import javax.jcr.Node;
037import javax.jcr.Property;
038import javax.jcr.PropertyType;
039import javax.jcr.RepositoryException;
040import javax.jcr.Value;
041
042import com.hp.hpl.jena.rdf.model.Resource;
043
044import org.fcrepo.kernel.api.models.FedoraResource;
045import org.fcrepo.kernel.api.exception.IdentifierConversionException;
046import org.fcrepo.kernel.api.exception.NoSuchPropertyDefinitionException;
047import org.fcrepo.kernel.api.identifiers.IdentifierConverter;
048import org.slf4j.Logger;
049
050/**
051 * Tools for replacing, appending and deleting JCR node properties
052 * @author Chris Beer
053 * @author ajs6f
054 * @since May 10, 2013
055 */
056public class NodePropertiesTools {
057
058    private static final Logger LOGGER = getLogger(NodePropertiesTools.class);
059
060    /**
061     * Given a JCR node, property and value, either:
062     *  - if the property is single-valued, replace the existing property with
063     *    the new value
064     *  - if the property is multivalued, append the new value to the property
065     * @param node the JCR node
066     * @param propertyName a name of a JCR property (either pre-existing or
067     *   otherwise)
068     * @param newValue the JCR value to insert
069     * @throws RepositoryException if repository exception occurred
070     */
071    public void appendOrReplaceNodeProperty(final Node node, final String propertyName, final Value newValue)
072        throws RepositoryException {
073
074        final Property property;
075
076        // if it already exists, we can take some shortcuts
077        if (node.hasProperty(propertyName)) {
078
079            property = node.getProperty(propertyName);
080
081            if (property.isMultiple()) {
082                LOGGER.debug("Appending value {} to {} property {}", newValue,
083                             PropertyType.nameFromValue(property.getType()),
084                             propertyName);
085
086                // if the property is multi-valued, go ahead and append to it.
087                final List<Value> newValues = new ArrayList<>(asList(node.getProperty(propertyName).getValues()));
088
089                if (!newValues.contains(newValue)) {
090                    newValues.add(newValue);
091                    property.setValue(newValues.toArray(new Value[newValues.size()]));
092                }
093            } else {
094                // or we'll just overwrite its single value
095                LOGGER.debug("Overwriting {} property {} with new value {}", PropertyType.nameFromValue(property
096                        .getType()), propertyName, newValue);
097                property.setValue(newValue);
098            }
099        } else {
100            // we're creating a new property on this node, so we check whether it should be multi-valued
101            boolean isMultiple = true;
102            try {
103                isMultiple = isMultivaluedProperty(node, propertyName);
104            } catch (final NoSuchPropertyDefinitionException e) {
105                // simply represents a new kind of property on this node
106            }
107            if (isMultiple) {
108                LOGGER.debug("Creating new multivalued {} property {} with " +
109                             "initial value [{}]",
110                             PropertyType.nameFromValue(newValue.getType()),
111                             propertyName, newValue);
112                property = node.setProperty(propertyName, new Value[]{newValue}, newValue.getType());
113            } else {
114                LOGGER.debug("Creating new single-valued {} property {} with " +
115                             "initial value {}",
116                             PropertyType.nameFromValue(newValue.getType()),
117                             propertyName, newValue);
118                property = node.setProperty(propertyName, newValue, newValue.getType());
119            }
120        }
121
122        if (!property.isMultiple() && !isInternalReferenceProperty.test(property)) {
123            final String referencePropertyName = getReferencePropertyName(propertyName);
124            if (node.hasProperty(referencePropertyName)) {
125                node.getProperty(referencePropertyName).remove();
126            }
127        }
128    }
129
130    /**
131     * Add a reference placeholder from one node to another in-domain resource
132     * @param idTranslator the id translator
133     * @param node the node
134     * @param propertyName the property name
135     * @param resource the resource
136     * @throws RepositoryException if repository exception occurred
137     */
138    public void addReferencePlaceholders(final IdentifierConverter<Resource,FedoraResource> idTranslator,
139                                          final Node node,
140                                          final String propertyName,
141                                          final Resource resource) throws RepositoryException {
142
143        try {
144            final Node refNode = getJcrNode(idTranslator.convert(resource));
145
146            if (isExternalNode.test(refNode)) {
147                // we can't apply REFERENCE properties to external resources
148                return;
149            }
150
151            final String referencePropertyName = getReferencePropertyName(propertyName);
152
153            if (!isMultivaluedProperty(node, propertyName)) {
154                if (node.hasProperty(referencePropertyName)) {
155                    node.getProperty(referencePropertyName).remove();
156                }
157
158                if (node.hasProperty(propertyName)) {
159                    node.getProperty(propertyName).remove();
160                }
161            }
162
163            final Value v = node.getSession().getValueFactory().createValue(refNode, true);
164            appendOrReplaceNodeProperty(node, referencePropertyName, v);
165
166        } catch (final IdentifierConversionException e) {
167            // no-op
168        }
169    }
170
171    /**
172     * Remove a reference placeholder that links one node to another in-domain resource
173     * @param idTranslator the id translator
174     * @param node the node
175     * @param propertyName the property name
176     * @param resource the resource
177     * @throws RepositoryException if repository exception occurred
178     */
179    public void removeReferencePlaceholders(final IdentifierConverter<Resource,FedoraResource> idTranslator,
180                                             final Node node,
181                                             final String propertyName,
182                                             final Resource resource) throws RepositoryException {
183
184        final String referencePropertyName = getReferencePropertyName(propertyName);
185
186        final Node refNode = getJcrNode(idTranslator.convert(resource));
187        final Value v = node.getSession().getValueFactory().createValue(refNode, true);
188        removeNodeProperty(node, referencePropertyName, v);
189    }
190    /**
191     * Given a JCR node, property and value, remove the value (if it exists)
192     * from the property, and remove the
193     * property if no values remove
194     *
195     * @param node the JCR node
196     * @param propertyName a name of a JCR property (either pre-existing or
197     *   otherwise)
198     * @param valueToRemove the JCR value to remove
199     * @throws RepositoryException if repository exception occurred
200     */
201    public void removeNodeProperty(final Node node, final String propertyName, final Value valueToRemove)
202        throws RepositoryException {
203        LOGGER.debug("Request to remove {}", valueToRemove);
204        // if the property doesn't exist, we don't need to worry about it.
205        if (node.hasProperty(propertyName)) {
206
207            final Property property = node.getProperty(propertyName);
208            final String strValueToRemove = valueToRemove.getString();
209
210            if (property.isMultiple()) {
211                final AtomicBoolean remove = new AtomicBoolean();
212                final Value[] newValues = stream(node.getProperty(propertyName).getValues()).filter(uncheck(v -> {
213                    final String strVal = v.getString();
214
215                    LOGGER.debug("v is '{}', valueToRemove is '{}'", v, strValueToRemove );
216                    if (strVal.equals(strValueToRemove)) {
217                        remove.set(true);
218                        return false;
219                    }
220
221                    return true;
222                })).toArray(Value[]::new);
223
224                // we only need to update the property if we did anything.
225                if (remove.get()) {
226                    if (newValues.length == 0) {
227                        LOGGER.debug("Removing property '{}'", propertyName);
228                        property.remove();
229                    } else {
230                        LOGGER.debug("Removing value '{}' from property '{}'", strValueToRemove, propertyName);
231                        property.setValue(newValues);
232                    }
233                } else {
234                    LOGGER.debug("Value not removed from property name '{}' (value '{}')", propertyName,
235                            strValueToRemove);
236                    throw new RepositoryException ("Property '" + propertyName + "': Unable to remove value '" +
237                            StringUtils.substring(strValueToRemove, 0, 50) + "...'");
238                }
239            } else {
240
241                final String strPropVal = property.getValue().getString();
242
243                LOGGER.debug("Removing string '{}'", strValueToRemove);
244                if (StringUtils.equals(strPropVal, strValueToRemove)) {
245                    LOGGER.debug("single value: Removing value from property '{}'", propertyName);
246                    property.remove();
247                } else {
248                    LOGGER.debug("Value not removed from property name '{}' (property value: '{}';compare value: '{}')",
249                            propertyName, strPropVal, strValueToRemove);
250                    throw new RepositoryException("Property '" + propertyName + "': Unable to remove value '" +
251                            StringUtils.substring(strValueToRemove, 0, 50) + "'");
252                }
253            }
254        }
255    }
256}