org.kuali.rice.krad.web.bind.UifViewBeanWrapper.java Source code

Java tutorial

Introduction

Here is the source code for org.kuali.rice.krad.web.bind.UifViewBeanWrapper.java

Source

/**
 * Copyright 2005-2014 The Kuali Foundation
 *
 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
 *
 * 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.kuali.rice.krad.web.bind;

import org.apache.commons.lang.ObjectUtils;
import org.kuali.rice.core.api.CoreApiServiceLocator;
import org.kuali.rice.core.api.encryption.EncryptionService;
import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
import org.kuali.rice.krad.uif.UifConstants;
import org.kuali.rice.krad.uif.lifecycle.ViewPostMetadata;
import org.kuali.rice.krad.uif.util.CopyUtils;
import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
import org.kuali.rice.krad.uif.view.ViewModel;
import org.omg.CORBA.Request;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.BeansException;
import org.springframework.beans.InvalidPropertyException;
import org.springframework.beans.NotReadablePropertyException;
import org.springframework.beans.NullValueInNestedPathException;
import org.springframework.beans.PropertyAccessorUtils;
import org.springframework.beans.PropertyValue;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.beans.PropertyDescriptor;
import java.beans.PropertyEditor;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Class is a top level BeanWrapper for a UIF View Model.
 *
 * <p>Registers custom property editors configured on the field associated with the property name for which
 * we are getting or setting a value. In addition determines if the field requires encryption and if so applies
 * the {@link UifEncryptionPropertyEditorWrapper}</p>
 *
 * @author Kuali Rice Team (rice.collab@kuali.org)
 */
public class UifViewBeanWrapper extends BeanWrapperImpl {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(UifViewBeanWrapper.class);

    // this stores all properties this wrapper has already checked
    // with the view so the service isn't called again
    private Set<String> processedProperties;

    private final UifBeanPropertyBindingResult bindingResult;

    public UifViewBeanWrapper(ViewModel model, UifBeanPropertyBindingResult bindingResult) {
        super(model);

        this.bindingResult = bindingResult;
        this.processedProperties = new HashSet<String>();
    }

    /**
     * Attempts to find a corresponding data field for the given property name in the current view or previous view,
     * then if the field has a property editor configured it is registered with the property editor registry to use
     * for this property.
     *
     * @param propertyName name of the property to find field and editor for
     */
    private void registerEditorFromView(String propertyName) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Attempting to find property editor for property '" + propertyName + "'");
        }

        // check if we already processed this property for this BeanWrapper instance
        if (processedProperties.contains(propertyName)) {
            return;
        }

        ViewPostMetadata viewPostMetadata = ((ViewModel) getWrappedInstance()).getViewPostMetadata();
        if (viewPostMetadata == null) {
            return;
        }

        PropertyEditor propertyEditor = viewPostMetadata.getFieldEditor(propertyName);
        if (propertyEditor != null) {
            registerCustomEditor(null, propertyName, propertyEditor);
        }

        processedProperties.add(propertyName);
    }

    /**
     * Finds a property editor for the given propert name, checking for a custom registered editor and editors
     * by type.
     *
     * @param propertyName name of the property to get editor for
     * @return property editor instance
     */
    protected PropertyEditor findEditorForPropertyName(String propertyName) {
        Class<?> clazz = getPropertyType(propertyName);
        if (LOG.isDebugEnabled()) {
            LOG.debug("Attempting retrieval of property editor using class '" + clazz + "' and property path '"
                    + propertyName + "'");
        }

        PropertyEditor editor = findCustomEditor(clazz, propertyName);
        if (editor == null) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("No custom property editor found using class '" + clazz + "' and property path '"
                        + propertyName + "'. Attempting to find default property editor class.");
            }
            editor = getDefaultEditor(clazz);
        }

        return editor;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Class<?> getPropertyType(String propertyName) throws BeansException {
        try {
            PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
            if (pd != null) {
                return pd.getPropertyType();
            }

            // Maybe an indexed/mapped property...
            Object value = super.getPropertyValue(propertyName);
            if (value != null) {
                return value.getClass();
            }

            // Check to see if there is a custom editor,
            // which might give an indication on the desired target type.
            Class<?> editorType = guessPropertyTypeFromEditors(propertyName);
            if (editorType != null) {
                return editorType;
            }
        } catch (InvalidPropertyException ex) {
            // Consider as not determinable.
        }

        return null;
    }

    /**
     * Overridden to copy property editor registration to the new bean wrapper.
     *
     * {@inheritDoc}
     */
    @Override
    protected BeanWrapperImpl getBeanWrapperForPropertyPath(String propertyPath) {
        BeanWrapperImpl beanWrapper = super.getBeanWrapperForPropertyPath(propertyPath);

        PropertyTokenHolder tokens = getPropertyNameTokens(propertyPath);
        String canonicalName = tokens.canonicalName;

        int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(canonicalName);
        if (pos != -1) {
            canonicalName = canonicalName.substring(0, pos);
        }

        copyCustomEditorsTo(beanWrapper, canonicalName);

        return beanWrapper;
    }

    /**
     * Overridden to register any property editor for the property before the value is pulled.
     *
     * {@inheritDoc}
     */
    @Override
    public Object getPropertyValue(String propertyName) throws BeansException {
        registerEditorFromView(propertyName);

        // set auto grow to false here because we don't want empty object created when only
        // displaying data
        setAutoGrowNestedPaths(false);

        Object value = null;
        try {
            value = super.getPropertyValue(propertyName);
        } catch (NullValueInNestedPathException e) {
            // swallow null values in path and return null as the value
        }

        return value;
    }

    /**
     * Overridden to perform processing before and after the value is set.
     *
     * <p>First binding security is checked to determine whether the path allows binding. Next,
     * access security is checked to determine whether the value needs decrypted. Finally, if
     * change tracking is enabled, the original value is compared with the new for indicating a
     * modified path.</p>
     *
     * {@inheritDoc}
     */
    @Override
    public void setPropertyValue(PropertyValue pv) throws BeansException {
        boolean isPropertyAccessible = checkPropertyBindingAccess(pv.getName());
        if (!isPropertyAccessible) {
            return;
        }

        Object value = processValueBeforeSet(pv.getName(), pv.getValue());

        pv = new PropertyValue(pv, value);

        // save off the original value if we are change tracking
        boolean originalValueSaved = true;
        Object originalValue = null;
        if (bindingResult.isChangeTracking()) {
            try {
                originalValue = getPropertyValue(pv.getName());
            } catch (Exception e) {
                // be failsafe here, if an exception happens here then we can't make any assumptions about whether
                // the property value changed or not
                originalValueSaved = false;
            }
        }

        // since auto grows is explicity turned off for get, we need to turn it on for set (so our objects
        // will grow if necessary for user entered data)
        setAutoGrowNestedPaths(true);

        // set the actual property value
        super.setPropertyValue(pv);

        // if we are change tracking and we saved original value, check if it's modified
        if (bindingResult.isChangeTracking() && originalValueSaved) {
            try {
                Object newValue = getPropertyValue(pv.getName());
                if (ObjectUtils.notEqual(originalValue, newValue)) {
                    // if they are not equal, it's been modified!
                    bindingResult.addModifiedPath(pv.getName());
                }
            } catch (Exception e) {
                // failsafe here as well
            }
        }
    }

    /**
     * Overridden to perform processing before and after the value is set.
     *
     * <p>First binding security is checked to determine whether the path allows binding. Next,
     * access security is checked to determine whether the value needs decrypted. Finally, if
     * change tracking is enabled, the original value is compared with the new for indicating a
     * modified path.</p>
     *
     * {@inheritDoc}
     */
    @Override
    public void setPropertyValue(String propertyName, Object value) throws BeansException {
        boolean isPropertyAccessible = checkPropertyBindingAccess(propertyName);
        if (!isPropertyAccessible) {
            return;
        }

        value = processValueBeforeSet(propertyName, value);

        // save off the original value
        boolean originalValueSaved = true;
        Object originalValue = null;
        try {
            originalValue = getPropertyValue(propertyName);
        } catch (Exception e) {
            // be failsafe here, if an exception happens here then we can't make any assumptions about whether
            // the property value changed or not
            originalValueSaved = false;
        }

        setAutoGrowNestedPaths(true);

        // set the actual property value
        super.setPropertyValue(propertyName, value);

        // only check if it's modified if we were able to save the original value
        if (originalValueSaved) {
            try {
                Object newValue = getPropertyValue(propertyName);
                if (ObjectUtils.notEqual(originalValue, newValue)) {
                    // if they are not equal, it's been modified!
                    bindingResult.addModifiedPath(propertyName);
                }
            } catch (Exception e) {
                // failsafe here as well
            }
        }
    }

    /**
     * Determines whether request binding is allowed for the given property name/path.
     *
     * <p>Binding access is determined by default based on the view's post metadata. A set of
     * accessible binding paths (populated during the view lifecycle) is maintained within this data.
     * Overrides can be specified using the annotations {@link org.kuali.rice.krad.web.bind.RequestProtected}
     * and {@link org.kuali.rice.krad.web.bind.RequestAccessible}.</p>
     *
     * <p>If the path is not accessible, it is recorded in the binding results suppressed fields. Controller
     * methods can accept the binding result and further handle these properties if necessary.</p>
     *
     * @param propertyName name/path of the property to check binding access for
     * @return boolean true if binding access is allowed, false if not allowed
     */
    protected boolean checkPropertyBindingAccess(String propertyName) {
        boolean isAccessible = false;

        // check for explicit property annotations that indicate access
        Boolean bindingAnnotationAccess = checkBindingAnnotationsInPath(propertyName);
        if (bindingAnnotationAccess != null) {
            isAccessible = bindingAnnotationAccess.booleanValue();
        } else {
            // default access, must be in view's accessible binding paths
            ViewPostMetadata viewPostMetadata = ((ViewModel) getWrappedInstance()).getViewPostMetadata();
            if ((viewPostMetadata != null) && (viewPostMetadata.getAccessibleBindingPaths() != null)) {
                isAccessible = viewPostMetadata.getAccessibleBindingPaths().contains(propertyName);
            }
        }

        if (!isAccessible) {
            LOG.debug("Request parameter sent for inaccessible binding path: " + propertyName);

            bindingResult.recordSuppressedField(propertyName);
        }

        return isAccessible;
    }

    /**
     * Determines whether one of the binding annotations is present within the given property path, and if
     * so returns whether access should be granted based on those annotation(s).
     *
     * <p>Binding annotations may occur anywhere in the property path. For example, if the path is 'object.field1',
     * a binding annotation may be present on the 'object' property or the 'field1' property. If multiple annotations
     * are found in the path, the annotation at the deepest level is taken. If both the protected and accessible
     * annotation are found at the same level, the protected access is used.</p>
     *
     * @param propertyPath path to look for annotations
     * @return Boolean true if an annotation is found and the access is allowed, false if an annotation is found
     * and the access is protected, null if no annotations where found in the path
     */
    protected Boolean checkBindingAnnotationsInPath(String propertyPath) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
                .getRequest();

        while (!StringUtils.isEmpty(propertyPath)) {
            String nestedPath = ObjectPropertyUtils.getPathTail(propertyPath);
            String parentPropertyPath = ObjectPropertyUtils.removePathTail(propertyPath);

            Class<?> parentPropertyClass = getWrappedClass();

            // for nested paths, we need to get the class of the immediate parent
            if (!StringUtils.isEmpty(parentPropertyPath)) {
                parentPropertyClass = ObjectPropertyUtils.getPropertyType(getWrappedInstance(), parentPropertyPath);
            }

            // remove index or map key to get the correct property name
            if (org.apache.commons.lang.StringUtils.endsWith(nestedPath, "]")) {
                nestedPath = org.apache.commons.lang.StringUtils.substringBefore(nestedPath, "[");
            }

            RequestProtected protectedAnnotation = (RequestProtected) CopyUtils
                    .getFieldAnnotation(parentPropertyClass, nestedPath, RequestProtected.class);
            if ((protectedAnnotation != null)
                    && annotationMatchesRequestMethod(protectedAnnotation.method(), request.getMethod())) {
                return Boolean.FALSE;
            }

            RequestAccessible accessibleAnnotation = (RequestAccessible) CopyUtils
                    .getFieldAnnotation(parentPropertyClass, nestedPath, RequestAccessible.class);
            if (accessibleAnnotation != null) {
                boolean isAnnotationRequestMethod = annotationMatchesRequestMethod(accessibleAnnotation.method(),
                        request.getMethod());
                boolean isAnnotationMethodToCalls = annotationMatchesMethodToCalls(
                        accessibleAnnotation.methodToCalls(),
                        request.getParameter(UifConstants.CONTROLLER_METHOD_DISPATCH_PARAMETER_NAME));
                if (isAnnotationRequestMethod && isAnnotationMethodToCalls) {
                    //((UifFormBase) this.bindingResult.getTarget()).getMethodToCall())) {
                    return Boolean.TRUE;
                }
            }

            propertyPath = parentPropertyPath;
        }

        return null;
    }

    /**
     * Indicates whether one of the given request accessible methods to call in the given array matches the
     * actual methodToCall of the request.
     *
     * @param annotationMethodToCalls array of request accessible methods to call to check against
     * @param methodToCall method to call of the request
     * @return boolean true if one of the annotation methods to call match, false if none match
     */
    protected boolean annotationMatchesMethodToCalls(String[] annotationMethodToCalls, String methodToCall) {
        // empty array of methods should match all
        if ((annotationMethodToCalls == null) || (annotationMethodToCalls.length == 0)) {
            return true;
        }

        for (String annotationMethodToCall : annotationMethodToCalls) {
            if (org.apache.commons.lang.StringUtils.equals(annotationMethodToCall, methodToCall)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Indicates whether one of the given request methods in the given array matches the actual method of
     * the request.
     *
     * @param annotationMethods array of request methods to check
     * @param requestMethod method of the request to match on
     * @return boolean true if one of the annotation methods match, false if none match
     */
    protected boolean annotationMatchesRequestMethod(RequestMethod[] annotationMethods, String requestMethod) {
        // empty array of methods should match all
        if ((annotationMethods == null) || (annotationMethods.length == 0)) {
            return true;
        }

        for (RequestMethod annotationMethod : annotationMethods) {
            if (org.apache.commons.lang.StringUtils.equals(annotationMethod.name(), requestMethod)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Registers any custom property editor for the property name/path, converts empty string values to null, and
     * calls helper method to decrypt secure values.
     *
     * @param propertyName name of the property
     * @param value value of the property to process
     * @return updated (possibly) property value
     */
    protected Object processValueBeforeSet(String propertyName, Object value) {
        registerEditorFromView(propertyName);

        Object processedValue = value;

        // Convert blank string values to null so empty strings are not set on the form as values (useful for legacy
        // checks) Jira: KULRICE-11424
        if (value instanceof String) {
            String propertyValue = (String) value;

            if (StringUtils.isEmpty(propertyValue)) {
                processedValue = null;
            } else {
                processedValue = decryptValueIfNecessary(propertyName, propertyValue);
            }
        }

        return processedValue;
    }

    /**
     * If the given property name is secure, decrypts the value by calling the encryption service.
     *
     * @param propertyName name of the property
     * @param propertyValue value of the property
     * @return String decrypted property value (or original value if not secure)
     */
    protected String decryptValueIfNecessary(String propertyName, String propertyValue) {
        String decryptedPropertyValue = propertyValue;

        if (propertyValue.endsWith(EncryptionService.ENCRYPTION_POST_PREFIX)) {
            propertyValue = org.apache.commons.lang.StringUtils.removeEnd(propertyValue,
                    EncryptionService.ENCRYPTION_POST_PREFIX);
        }

        if (isSecure(getWrappedClass(), propertyName)) {
            try {
                if (CoreApiServiceLocator.getEncryptionService().isEnabled()) {
                    decryptedPropertyValue = CoreApiServiceLocator.getEncryptionService().decrypt(propertyValue);
                }
            } catch (GeneralSecurityException e) {
                throw new RuntimeException(e);
            }
        }

        return decryptedPropertyValue;
    }

    /**
     * Checks whether the given property is secure.
     *
     * @param wrappedClass class the property is associated with
     * @param propertyPath path to the property
     * @return boolean true if the property is secure, false if not
     */
    protected boolean isSecure(Class<?> wrappedClass, String propertyPath) {
        if (KRADServiceLocatorWeb.getDataObjectAuthorizationService()
                .attributeValueNeedsToBeEncryptedOnFormsAndLinks(wrappedClass, propertyPath)) {
            return true;
        }

        BeanWrapperImpl beanWrapper;
        try {
            beanWrapper = getBeanWrapperForPropertyPath(propertyPath);
        } catch (NotReadablePropertyException nrpe) {
            LOG.debug("Bean wrapper was not found for " + propertyPath
                    + ", but since it cannot be accessed it will not be set as secure.", nrpe);
            return false;
        }

        if (org.apache.commons.lang.StringUtils.isNotBlank(beanWrapper.getNestedPath())) {
            PropertyTokenHolder tokens = getPropertyNameTokens(propertyPath);
            String nestedPropertyPath = org.apache.commons.lang.StringUtils.removeStart(tokens.canonicalName,
                    beanWrapper.getNestedPath());

            return isSecure(beanWrapper.getWrappedClass(), nestedPropertyPath);
        }

        return false;
    }

    /**
     * Parse the given property name into the corresponding property name tokens.
     *
     * @param propertyName the property name to parse
     * @return representation of the parsed property tokens
     */
    private PropertyTokenHolder getPropertyNameTokens(String propertyName) {
        PropertyTokenHolder tokens = new PropertyTokenHolder();
        String actualName = null;
        List<String> keys = new ArrayList<String>(2);
        int searchIndex = 0;
        while (searchIndex != -1) {
            int keyStart = propertyName.indexOf(PROPERTY_KEY_PREFIX, searchIndex);
            searchIndex = -1;
            if (keyStart != -1) {
                int keyEnd = propertyName.indexOf(PROPERTY_KEY_SUFFIX, keyStart + PROPERTY_KEY_PREFIX.length());
                if (keyEnd != -1) {
                    if (actualName == null) {
                        actualName = propertyName.substring(0, keyStart);
                    }
                    String key = propertyName.substring(keyStart + PROPERTY_KEY_PREFIX.length(), keyEnd);
                    if ((key.startsWith("'") && key.endsWith("'"))
                            || (key.startsWith("\"") && key.endsWith("\""))) {
                        key = key.substring(1, key.length() - 1);
                    }
                    keys.add(key);
                    searchIndex = keyEnd + PROPERTY_KEY_SUFFIX.length();
                }
            }
        }
        tokens.actualName = (actualName != null ? actualName : propertyName);
        tokens.canonicalName = tokens.actualName;
        if (!keys.isEmpty()) {
            tokens.canonicalName += PROPERTY_KEY_PREFIX
                    + StringUtils.collectionToDelimitedString(keys, PROPERTY_KEY_SUFFIX + PROPERTY_KEY_PREFIX)
                    + PROPERTY_KEY_SUFFIX;
            tokens.keys = StringUtils.toStringArray(keys);
        }
        return tokens;
    }

    private static class PropertyTokenHolder {

        public String canonicalName;

        public String actualName;

        public String[] keys;
    }
}