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