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     * @throws RepositoryException
063     */
064    public Property appendOrReplaceNodeProperty(final Node node, final String propertyName, final Value newValue)
065        throws RepositoryException {
066
067        final Property property;
068
069        // if it already exists, we can take some shortcuts
070        if (node.hasProperty(propertyName)) {
071
072            property = node.getProperty(propertyName);
073
074            if (property.isMultiple()) {
075                LOGGER.debug("Appending value {} to {} property {}", newValue,
076                             PropertyType.nameFromValue(property.getType()),
077                             propertyName);
078
079                // if the property is multi-valued, go ahead and append to it.
080                final List<Value> newValues = new ArrayList<>();
081                Collections.addAll(newValues,
082                                   node.getProperty(propertyName).getValues());
083
084                if (!newValues.contains(newValue)) {
085                    newValues.add(newValue);
086                    property.setValue(toArray(newValues, Value.class));
087                }
088            } else {
089                // or we'll just overwrite it
090                LOGGER.debug("Overwriting {} property {} with new value {}", PropertyType.nameFromValue(property
091                        .getType()), propertyName, newValue);
092                property.setValue(newValue);
093            }
094        } else {
095            boolean isMultiple = true;
096            try {
097                isMultiple = isMultivaluedProperty(node, propertyName);
098
099            } catch (final NoSuchPropertyDefinitionException e) {
100                // simply represents a new kind of property on this node
101            }
102            if (isMultiple) {
103                LOGGER.debug("Creating new multivalued {} property {} with " +
104                             "initial value [{}]",
105                             PropertyType.nameFromValue(newValue.getType()),
106                             propertyName, newValue);
107                property = node.setProperty(propertyName, new Value[]{newValue}, newValue.getType());
108            } else {
109                LOGGER.debug("Creating new single-valued {} property {} with " +
110                             "initial value {}",
111                             PropertyType.nameFromValue(newValue.getType()),
112                             propertyName, newValue);
113                property = node.setProperty(propertyName, newValue, newValue.getType());
114            }
115        }
116
117        if (!property.isMultiple() && !isInternalReferenceProperty.apply(property)) {
118            final String referencePropertyName = getReferencePropertyName(propertyName);
119            if (node.hasProperty(referencePropertyName)) {
120                node.setProperty(referencePropertyName, (Value[]) null);
121            }
122        }
123
124        return property;
125    }
126
127    /**
128     * Add a reference placeholder from one node to another in-domain resource
129     * @param idTranslator
130     * @param node
131     * @param propertyName
132     * @param resource
133     * @throws RepositoryException
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 (isExternal.apply(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.setProperty(referencePropertyName, (Value[]) null);
153                }
154
155                if (node.hasProperty(propertyName)) {
156                    node.setProperty(propertyName, (Value) null);
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
171     * @param node
172     * @param propertyName
173     * @param resource
174     * @throws RepositoryException
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
197     */
198    public Property removeNodeProperty(final Node node, final String propertyName, final Value valueToRemove)
199        throws RepositoryException {
200        final Property property;
201
202        // if the property doesn't exist, we don't need to worry about it.
203        if (node.hasProperty(propertyName)) {
204
205            property = node.getProperty(propertyName);
206
207            if (JcrPropertyFunctions.isMultipleValuedProperty.apply(property)) {
208
209                final List<Value> newValues = new ArrayList<>();
210
211                boolean remove = false;
212
213                for (final Value v : node.getProperty(propertyName).getValues()) {
214                    if (v.equals(valueToRemove)) {
215                        remove = true;
216                    } else {
217                        newValues.add(v);
218                    }
219                }
220
221                // we only need to update the property if we did anything.
222                if (remove) {
223                    if (newValues.isEmpty()) {
224                        LOGGER.debug("Removing property {}", propertyName);
225                        property.setValue((Value[])null);
226                    } else {
227                        LOGGER.debug("Removing value {} from property {}",
228                                     valueToRemove, propertyName);
229                        property
230                            .setValue(toArray(newValues, Value.class));
231                    }
232                }
233
234            } else {
235                if (property.getValue().equals(valueToRemove)) {
236                    LOGGER.debug("Removing value from property {}", propertyName);
237                    property.setValue((Value)null);
238                }
239            }
240        } else {
241            property = null;
242        }
243
244        return property;
245    }
246}