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