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