| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.sling.servlets.post.impl.helper; |
| |
| import java.math.BigDecimal; |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import javax.jcr.PropertyType; |
| |
| import org.apache.jackrabbit.JcrConstants; |
| import org.apache.sling.api.resource.ModifiableValueMap; |
| import org.apache.sling.api.resource.PersistenceException; |
| import org.apache.sling.api.resource.Resource; |
| import org.apache.sling.servlets.post.Modification; |
| import org.apache.sling.servlets.post.SlingPostConstants; |
| import org.apache.sling.servlets.post.exceptions.PreconditionViolatedPersistenceException; |
| |
| /** |
| * Sets a property on the given resource, in some cases with a specific type and |
| * value. For example, "lastModified" with an empty value is stored as the |
| * current Date. |
| * Special handling might apply if the resource is backed by a JCR node. |
| */ |
| public class SlingPropertyValueHandler { |
| |
| /** |
| * Defines a map of auto properties |
| */ |
| private static final Map<String, AutoType> AUTO_PROPS = new HashMap<>(); |
| static { |
| AUTO_PROPS.put("created", AutoType.CREATED); |
| AUTO_PROPS.put("createdBy", AutoType.CREATED_BY); |
| AUTO_PROPS.put(JcrConstants.JCR_CREATED, AutoType.CREATED); |
| AUTO_PROPS.put("jcr:createdBy", AutoType.CREATED_BY); |
| AUTO_PROPS.put("lastModified", AutoType.MODIFIED); |
| AUTO_PROPS.put("lastModifiedBy", AutoType.MODIFIED_BY); |
| AUTO_PROPS.put(JcrConstants.JCR_LASTMODIFIED, AutoType.MODIFIED); |
| AUTO_PROPS.put("jcr:lastModifiedBy", AutoType.MODIFIED_BY); |
| } |
| |
| /** |
| * the post processor |
| */ |
| private final List<Modification> changes; |
| |
| private final DateParser dateParser; |
| |
| private final JCRSupport jcrSupport; |
| |
| /** |
| * current date for all properties in this request |
| */ |
| private final Calendar now = Calendar.getInstance(); |
| |
| /** |
| * Constructs a property value handler |
| * @param dateParser the dateParser |
| * @param jcrSupport the jcrSupport |
| * @param changes the changes |
| */ |
| public SlingPropertyValueHandler(final DateParser dateParser, |
| final JCRSupport jcrSupport, |
| final List<Modification> changes) { |
| this.dateParser = dateParser; |
| this.jcrSupport = jcrSupport; |
| this.changes = changes; |
| } |
| |
| /** Return the AutoType for a given property name |
| * @return null if not found |
| * */ |
| static AutoType getAutoType(String propertyName) { |
| return AUTO_PROPS.get(propertyName); |
| } |
| |
| /** |
| * Set property on given node, with some automatic values when user provides |
| * the field name but no value. |
| * |
| * html example for testing: |
| * {@code |
| * <input type="hidden" name="created"/> |
| * <input type="hidden" name="lastModified"/> |
| * <input type="hidden" name="createdBy" /> |
| * <input type="hidden" name="lastModifiedBy"/> |
| * } |
| * |
| * @param parent the parent node |
| * @param prop the request property |
| * @throws PersistenceException if a resource error occurs |
| */ |
| public void setProperty(final Resource parent, final RequestProperty prop) |
| throws PersistenceException { |
| final Modifiable mod = new Modifiable(); |
| mod.resource = parent; |
| mod.node = jcrSupport.getNode(parent); |
| mod.valueMap = parent.adaptTo(ModifiableValueMap.class); |
| if ( mod.valueMap == null ) { |
| throw new PreconditionViolatedPersistenceException("Resource at '" + parent.getPath() + "' is not modifiable."); |
| } |
| |
| final String name = prop.getName(); |
| if (prop.providesValue()) { |
| // if user provided a value, don't mess with it |
| setPropertyAsIs(mod, prop); |
| |
| } else if (AUTO_PROPS.containsKey(name)) { |
| // check if this is a JCR resource and check node type |
| if ( this.jcrSupport.isPropertyProtectedOrNewAutoCreated(mod.node, name) ) { |
| return; |
| } |
| |
| // avoid collision with protected properties |
| final boolean isNew = jcrSupport.isNewNode(mod.node); |
| switch (getAutoType(name)) { |
| case CREATED: |
| if (isNew) { |
| setCurrentDate(mod, name); |
| } |
| break; |
| case CREATED_BY: |
| if (isNew) { |
| setCurrentUser(mod, name); |
| } |
| break; |
| case MODIFIED: |
| setCurrentDate(mod, name); |
| break; |
| case MODIFIED_BY: |
| setCurrentUser(mod, name); |
| break; |
| } |
| } else { |
| // no magic field, set value as provided |
| setPropertyAsIs(mod, prop); |
| } |
| } |
| |
| /** |
| * Sets the property to the given date |
| * @param parent parent resource |
| * @param name name of the property |
| * @throws PersistenceException if a resource error occurs |
| */ |
| private void setCurrentDate(final Modifiable parent, final String name) |
| throws PersistenceException { |
| removePropertyIfExists(parent, name); |
| parent.valueMap.put(name, now); |
| changes.add(Modification.onModified(parent.resource.getPath() + '/' + name)); |
| } |
| |
| /** |
| * set property to the current User id |
| * @param parent parent resource |
| * @param name name of the property |
| * @throws PersistenceException if a resource error occurs |
| */ |
| private void setCurrentUser(final Modifiable parent, final String name) |
| throws PersistenceException { |
| removePropertyIfExists(parent, name); |
| final String user = parent.resource.getResourceResolver().getUserID(); |
| parent.valueMap.put(name, user); |
| changes.add(Modification.onModified(parent.resource.getPath() + '/' + name)); |
| } |
| |
| /** |
| * Removes the property with the given name from the parent resource if it |
| * exists and if it's not a mandatory property. |
| * |
| * @param parent the parent resource |
| * @param name the name of the property to remove |
| * @return path of the property that was removed or <code>null</code> if |
| * it was not removed |
| * @throws PersistenceException if a repository error occurs. |
| */ |
| private String removePropertyIfExists(final Modifiable parent, final String name) |
| throws PersistenceException { |
| if (parent.valueMap.containsKey(name) && !jcrSupport.isPropertyMandatory(parent.node, name)) { |
| parent.valueMap.remove(name); |
| return parent.resource.getPath() + '/' + name; |
| } |
| return null; |
| } |
| |
| /** |
| * set property without processing, except for type hints |
| * |
| * @param parent the parent resource |
| * @param prop the request property |
| * @throws PersistenceException if a resource error occurs. |
| */ |
| private void setPropertyAsIs(final Modifiable parent, final RequestProperty prop) |
| throws PersistenceException { |
| |
| String[] values = prop.getStringValues(); |
| |
| // RequestProperty#getStringValues already takes care of the configs ignoreBlanks, defaultValues etc. |
| // and provides values as null, new String[0] etc. accordingly. |
| if (values == null || (values.length == 1 && values[0].length() == 0)) { |
| // if no value is present or a single empty string is given, |
| // just remove the existing property (if any) |
| removeProperty(parent, prop); |
| |
| } else if (values.length == 0) { |
| // do not create new prop here, but clear existing |
| clearProperty(parent, prop); |
| |
| } else { |
| // when patching, simply update the value list using the patch operations |
| if (prop.isPatch()) { |
| values = patch(parent, prop.getName(), values); |
| if (values == null) { |
| return; |
| } |
| } |
| |
| final boolean multiValue = isMultiValue(parent, prop, values); |
| |
| if (multiValue) { |
| // converting single into multi value props requires deleting it first |
| removeIfSingleValueProperty(parent, prop); |
| } |
| |
| final int type = getType(parent, prop); |
| if (jcrSupport.hasSession(parent.resource.getResourceResolver())) { |
| |
| if (type == PropertyType.DATE) { |
| if (storeAsDate(parent, prop.getName(), values, multiValue)) { |
| return; |
| } |
| } else if (isReferencePropertyType(type)) { |
| if (storeAsReference(parent, prop.getName(), values, type, multiValue)) { |
| return; |
| } |
| } |
| } |
| |
| store(parent, prop.getName(), values, type, multiValue); |
| } |
| } |
| |
| /** |
| * Patches a multi-value property using add and remove operations per value. |
| */ |
| private String[] patch(final Modifiable parent, String name, String[] values) |
| throws PersistenceException { |
| // we do not use a Set here, as we want to be very restrictive in our |
| // actions and avoid touching elements that are not modified through the |
| // add/remove patch operations; e.g. if the value "foo" occurs twice |
| // in the existing array, and is not touched, afterwards there should |
| // still be two times "foo" in the list, even if this is not a real set. |
| List<String> oldValues = new ArrayList<>(); |
| |
| if (parent.valueMap.containsKey(name)) { |
| if ( parent.node != null && !jcrSupport.isPropertyMultiple(parent.node, name)) { |
| |
| // can only patch multi-value props |
| return null; |
| } |
| |
| final String[] setValues = parent.valueMap.get(name, String[].class); |
| if ( setValues != null ) { |
| for(final String v : setValues) { |
| oldValues.add(v); |
| } |
| } |
| } |
| |
| boolean modified = false; |
| for (String v : values) { |
| if (v != null && v.length() > 0) { |
| final char op = v.charAt(0); |
| final String val = v.substring(1); |
| |
| if (op == SlingPostConstants.PATCH_ADD) { |
| if (!oldValues.contains(val)) { |
| oldValues.add(val); |
| modified = true; |
| } |
| } else if (op == SlingPostConstants.PATCH_REMOVE) { |
| while (oldValues.remove(val)) { |
| modified = true; |
| } |
| } |
| } |
| } |
| |
| // if the patch does not include any operations (e.g. invalid ops) |
| // return null to indicate that nothing should be done |
| if (modified) { |
| return oldValues.toArray(new String[oldValues.size()]); |
| } |
| |
| return null; |
| } |
| |
| |
| private boolean isReferencePropertyType(int propertyType) { |
| return propertyType == PropertyType.REFERENCE || propertyType == PropertyType.WEAKREFERENCE; |
| } |
| |
| /** |
| * Returns the property type to use for the given property. This is defined |
| * either by an explicit type hint in the request or simply the type of the |
| * existing property. |
| */ |
| private int getType(final Modifiable parent, RequestProperty prop) |
| throws PersistenceException { |
| // no explicit typehint |
| int type = PropertyType.UNDEFINED; |
| if (prop.getTypeHint() != null) { |
| try { |
| type = PropertyType.valueFromName(prop.getTypeHint()); |
| } catch (Exception e) { |
| // ignore |
| } |
| } |
| String[] values = prop.getStringValues(); |
| if ( type == PropertyType.UNDEFINED && values != null && values.length > 0 ) { |
| final Integer jcrType = jcrSupport.getPropertyType(parent.node, prop.getName()); |
| if ( jcrType != null ) { |
| type = jcrType; |
| } |
| } |
| return type; |
| } |
| |
| /** |
| * Returns whether the property should be handled as multi-valued. |
| */ |
| private boolean isMultiValue(final Modifiable parent, RequestProperty prop, String[] values) |
| throws PersistenceException { |
| // multiple values are provided |
| if (values != null && values.length > 1) { |
| return true; |
| } |
| // TypeHint with [] |
| if (prop.hasMultiValueTypeHint()) { |
| return true; |
| } |
| // patch method requires multi value |
| if (prop.isPatch()) { |
| return true; |
| } |
| // nothing in the request, so check the current JCR property definition |
| final Object value = parent.valueMap.get(prop.getName()); |
| if ( parent.node != null ) { |
| if ( value != null ) { |
| return jcrSupport.isPropertyMultiple(parent.node, prop.getName()); |
| } |
| } else { |
| if ( value != null && value.getClass().isArray() ) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Clears a property: sets an empty string for single-value properties, and |
| * removes multi-value properties. |
| */ |
| private void clearProperty(final Modifiable parent, RequestProperty prop) |
| throws PersistenceException { |
| if (parent.valueMap.containsKey(prop.getName())) { |
| if ( jcrSupport.isPropertyMultiple(parent.node, prop.getName()) ) { |
| // the existing property is multi-valued, so just delete it? |
| final String removePath = removePropertyIfExists(parent, prop.getName()); |
| if ( removePath != null ) { |
| changes.add(Modification.onDeleted(removePath)); |
| } |
| } else { |
| parent.valueMap.put(prop.getName(), ""); |
| changes.add(Modification.onModified(parent.resource.getPath() + '/' + prop.getName())); |
| } |
| } |
| } |
| |
| /** |
| * Removes the property if it exists. |
| */ |
| private void removeProperty(final Modifiable parent, final RequestProperty prop) |
| throws PersistenceException { |
| final String removePath = removePropertyIfExists(parent, prop.getName()); |
| if ( removePath != null ) { |
| changes.add(Modification.onDeleted(removePath)); |
| } |
| } |
| |
| /** |
| * Removes the property if it exists and is single-valued. |
| */ |
| private void removeIfSingleValueProperty(final Modifiable parent, |
| final RequestProperty prop) |
| throws PersistenceException { |
| if (parent.valueMap.containsKey(prop.getName())) { |
| if ( jcrSupport.isPropertyMultiple(parent.node, prop.getName()) ) { |
| // do nothing, multi value |
| return; |
| } |
| final String removePath = removePropertyIfExists(parent, prop.getName()); |
| if ( removePath != null ) { |
| changes.add(Modification.onDeleted(removePath)); |
| } |
| } |
| } |
| |
| /** |
| * Parses the given source strings and returns the respective Calendar value |
| * instances. If no format matches for any of the sources |
| * returns <code>null</code>. |
| * <p/> |
| * |
| * @param sources date time source strings |
| * @return Calendar value representations of the source or <code>null</code> |
| */ |
| private Calendar[] parse(final String sources[]) { |
| final Calendar ret[] = new Calendar[sources.length]; |
| for (int i=0; i< sources.length; i++) { |
| final Calendar c = dateParser.parse(sources[i]); |
| if (c == null) { |
| return null; |
| } |
| ret[i] = c; |
| } |
| return ret; |
| } |
| |
| |
| /** |
| * Stores property value(s) as date(s). Will parse the date(s) from the string |
| * value(s) in the {@link RequestProperty}. |
| * |
| * @return true only if parsing was successful and the property was actually changed |
| */ |
| private boolean storeAsDate(final Modifiable parent, String name, String[] values, boolean multiValued) |
| throws PersistenceException { |
| if (multiValued) { |
| final Calendar[] array = parse(values); |
| if (array != null) { |
| parent.valueMap.put(name, array); |
| changes.add(Modification.onModified(parent.resource.getPath() + '/' + name)); |
| return true; |
| } |
| } else { |
| if (values.length >= 1) { |
| final Calendar c = dateParser.parse(values[0]); |
| if (c != null) { |
| parent.valueMap.put(name, c); |
| changes.add(Modification.onModified(parent.resource.getPath() + '/' + name)); |
| |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Stores property value(s) as reference(s). Will parse the reference(s) from the string |
| * value(s) in the {@link RequestProperty}. |
| * |
| * @return true only if parsing was successful and the property was actually changed |
| */ |
| private boolean storeAsReference(final Modifiable parent, |
| final String name, |
| final String[] values, |
| final int type, |
| final boolean multiValued) |
| throws PersistenceException { |
| final Modification mod = this.jcrSupport.storeAsReference(parent.resource, parent.node, name, values, type, multiValued); |
| return mod != null; |
| } |
| |
| /** |
| * Stores the property as string or via a string value, but with an explicit |
| * type. Both multi-value or single-value. |
| */ |
| private void store(final Modifiable parent, |
| final String name, |
| final String[] values, |
| final int type, |
| final boolean multiValued) |
| throws PersistenceException { |
| if ( parent.node != null && type != PropertyType.UNDEFINED ) { |
| jcrSupport.setTypedProperty(parent.node, name, values, type, multiValued); |
| |
| } else { |
| if (multiValued) { |
| parent.valueMap.put(name, toJavaObject(values, type)); |
| } else if (values.length >= 1) { |
| parent.valueMap.put(name, toJavaObject(values[0], type)); |
| } |
| } |
| changes.add(Modification.onModified(parent.resource.getPath() + '/' + name)); |
| } |
| |
| /** Converts a value */ |
| private static Object toJavaObject(final String value, final int type) { |
| final boolean isEmpty = value == null || value.trim().length() == 0; |
| switch (type) { |
| case PropertyType.DECIMAL: |
| return isEmpty ? BigDecimal.ZERO : new BigDecimal(value); |
| case PropertyType.BOOLEAN: |
| return isEmpty ? Boolean.FALSE : Boolean.valueOf(value); |
| case PropertyType.DOUBLE: |
| return isEmpty ? (double)0.0 : Double.valueOf(value); |
| case PropertyType.LONG: |
| return isEmpty ? 0 : Long.valueOf(value); |
| default: // fallback |
| return value; |
| } |
| } |
| |
| /** Converts a value */ |
| private static Object toJavaObject(final String values[], final int type) { |
| final Object[] result = new Object[values.length]; |
| for (int i = 0; i < values.length; i++) { |
| if (values[i] != null ) { |
| result[i] = toJavaObject(values[i], type); |
| } |
| } |
| return result; |
| } |
| |
| |
| /** |
| * Defines an auto property behavior |
| */ |
| private enum AutoType { |
| CREATED, |
| CREATED_BY, |
| MODIFIED, |
| MODIFIED_BY |
| } |
| |
| public final static class Modifiable { |
| public Resource resource; |
| public ModifiableValueMap valueMap; |
| public Object node; |
| } |
| } |