org.terasoluna.gfw.web.el.ObjectToMapConverter.java Source code

Java tutorial

Introduction

Here is the source code for org.terasoluna.gfw.web.el.ObjectToMapConverter.java

Source

/*
 * Copyright (C) 2013-2016 terasoluna.org
 *
 * Licensed 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.terasoluna.gfw.web.el;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.util.*;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Convert {@code Object} to {@code Map<String, String>} using {@link FormattingConversionService}.
 * <p>
 * The converted Map can be populated to the original Object using {@link org.springframework.validation.DataBinder}.
 * </p>
 * <h3>Example1</h3>
 *
 * <pre>
 * <code>
 * class SearchUserForm {
 *   SearchUserCriteriaForm criteria;
 *   Boolean rememberCriteria;
 * }
 * </code>
 * </pre>
 *
 * <pre>
 * <code>
 * class SearchUserCriteriaForm {
 *   String name;
 *   Integer age;
 * }
 * </code>
 * </pre>
 * <p>
 * {@code SearchUserForm} instance will be converted as follows {@code "rememberCriteria":"true", "criteria.name":"yamada",
 * "criteria.age":"20"}
 * </p>
 * <h3>Example2</h3>
 *
 * <pre>
 * <code>
 * class BatchUpdateUserForm {
 *   List{@literal <UpdateUserCriteriaForm>} criteria;
 *   LogicalOperator operator;
 * }
 * </code>
 * </pre>
 *
 * <pre>
 * <code>
 * enum LogicalOperator {
 *   AND, OR
 * }
 * </code>
 * </pre>
 *
 * <pre>
 * <code>
 * class UpdateUserForm {
 *   String name;
 *   Integer age;
 * }
 * </code>
 * </pre>
 * <p>
 * {@code BatchUpdateUserForm} instance will be converted as follows {@code "criteria[0].name":"yamada",
 * "criteria[0].age":"20", "criteria[1].name":"tanaka", "criteria[1].name":"50", "operator":"AND"}
 * </p>
 * <h3>Example3</h3>
 *
 * <pre>
 * <code>class SearchAndBatchUpdateUserForm {
 *   SearchUserCriteriaForm criteria;
 *   List{@literal <User>} users;
 * }
 * </code>
 * </pre>
 *
 * <pre>
 * <code>
 * class SearchUserCriteriaForm {
 *   String name;
 *   Integer age;
 * }
 * </code>
 * </pre>
 *
 * <pre>
 * <code>
 * class User {
 *   String name;
 *   Integer age;
 * }
 * </code>
 * </pre>
 * <p>
 * {@code SearchUserCriteriaForm} instance will be converted as follows
 * {@code criteria.name=suzuki&criteria.age=30&users[0].name=yamada&users[0].age=20&users[1].name=tanaka&users[1].age=50}
 * </p>
 * <p>
 * If the value of a property is {@code null}, the value is converted to an empty string and the key is prefixed with
 * {@code "_"}. Request parameter that start with {@code "_"} is reset parameter provided by Spring Web MVC. If a reset
 * parameter is specified, Spring Web MVC bind {@code null} to a property value.<br>
 * e.g.) {@code "_rememberCriteria":"", "_criteria.name":"", "_criteria.age":""}
 * </p>
 * <p>
 * If the value of a {@link Iterable} or Array property is empty element, the value is converted to an empty string. If a empty
 * string is specified, Spring Web MVC bind empty element to a property value.<br>
 * e.g.) {@code "users":"", "criteria.name":"suzuki", "criteria.age":"30"}
 * </p>
 * <p>
 * If the value of a {@link Map} property is empty element, it is not converted. As a result, the property value will be a
 * default value of server side implementation because Spring Web MVC does not bind request parameter to the property value.
 * </p>
 * @since 5.0.1
 */
class ObjectToMapConverter {
    /**
     * type descriptor of string for format a value.
     */
    private static final TypeDescriptor STRING_DESC = TypeDescriptor.valueOf(String.class);

    /**
     * conversion service for formatting a value.
     */
    private final FormattingConversionService conversionService;

    /**
     * Constructor
     * @param conversionService {@link FormattingConversionService} to use. must not be null
     */
    public ObjectToMapConverter(FormattingConversionService conversionService) {
        Assert.notNull(conversionService, "'conversionService' must not be null");
        this.conversionService = conversionService;
    }

    /**
     * Convert the given map to the flatten map
     * @param prefix prefix of the key
     * @param value map instance to convert
     * @return converted map. all keys are prefixed with the given key
     */
    private Map<String, String> convert(String prefix, Map value) {
        Map<String, String> map = new LinkedHashMap<String, String>();
        for (Object entry : value.entrySet()) {
            Map.Entry e = (Map.Entry) entry;
            if (StringUtils.isEmpty(prefix)) {
                map.putAll(this.convert(e.getKey().toString(), e.getValue()));
            } else {
                map.putAll(this.convert(prefix + "[" + e.getKey() + "]", e.getValue()));
            }
        }
        return map;
    }

    /**
     * Convert the given Iterable to the flatten map
     * @param prefix prefix of the key
     * @param value iterable instance to convert
     * @return converted map. all keys are prefixed with the given key. If given Iterable is empty, the pair of the given
     *         name(prefix) and an empty string is added into map of return value.
     */
    private Map<String, String> convert(String prefix, Iterable value) {
        Map<String, String> map = new LinkedHashMap<String, String>();
        Iterator iterator = value.iterator();
        if (!iterator.hasNext()) {
            map.put(prefix, "");
            return map;
        }
        int i = 0;
        while (iterator.hasNext()) {
            map.putAll(this.convert(prefix + "[" + i + "]", iterator.next()));
            i++;
        }
        return map;
    }

    /**
     * Convert the given object to the flattened map.
     * <p>
     * Return empty map if the object is null.
     * </p>
     * @param object object to convert
     * @return converted map.
     * @see ObjectToMapConverter
     */
    public Map<String, String> convert(Object object) {
        if (object == null) {
            return Collections.emptyMap();
        }
        return this.convert("", object);
    }

    /**
     * Convert the given object to the flattened map.
     * <p>
     * Return empty map if the object is null.
     * </p>
     * @param prefix prefix of the key
     * @param object object to convert
     * @return converted map. all keys are prefixed with the given key
     * @see ObjectToMapConverter
     */
    public Map<String, String> convert(String prefix, Object object) {
        Map<String, String> map = new LinkedHashMap<String, String>();

        // at first, try to flatten the given object
        if (flatten(map, "", prefix, object, null)) {
            return map;
        }

        // the given object is a Java Bean
        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(object);
        PropertyDescriptor[] pds = beanWrapper.getPropertyDescriptors();

        // flatten properties in the given object
        for (PropertyDescriptor pd : pds) {
            String name = pd.getName();
            if ("class".equals(name) || !beanWrapper.isReadableProperty(name)) {
                continue;
            }
            Object value = beanWrapper.getPropertyValue(name);
            TypeDescriptor sourceType = beanWrapper.getPropertyTypeDescriptor(name);

            if (!flatten(map, prefix, name, value, sourceType)) {
                // the property can be a Java Bean
                // convert recursively
                Map<String, String> subMap = this.convert(name, value);
                map.putAll(subMap);
            }
        }

        return map;
    }

    /**
     * Add the pair of the given name and value to the given map. The value is flattened if required to be available in query
     * params. <br>
     * Return whether the value is flattened. The value is flattened in the case of the following types:
     * <ul>
     * <li>Array (unless the name is empty)</li>
     * <li>Iterable (unless the name is empty)</li>
     * <li>Map</li>
     * <li>{@link BeanUtils#isSimpleProperty(java.lang.Class)}</li>
     * <li>if {@link #conversionService} can convert</li>
     * </ul>
     * <p>
     * The value is formatted using {@link FormattingConversionService} is possible. If the value of a property is {@code null},
     * the value is converted to an empty string and the key is prefixed with {@code "_"}. Request parameter that start with
     * {@code "_"} is reset parameter provided by Spring Web MVC. If a reset parameter is specified, Spring Web MVC bind
     * {@code null} to a property value.
     * </p>
     * @param map map to add
     * @param prefix prefix of the key
     * @param name name of the value
     * @param value value to convert
     * @param sourceType {@link TypeDescriptor} to use
     * @return flatten map
     */
    private boolean flatten(Map<String, String> map, String prefix, String name, Object value,
            TypeDescriptor sourceType) {
        String key = StringUtils.isEmpty(prefix) ? name : prefix + "." + name;
        if (value == null) {
            String resetKey = "_" + key;
            map.put(resetKey, "");
            // the value has been flatten
            return true;
        }
        Class<?> clazz = value.getClass();
        if (value instanceof Iterable) {
            if (StringUtils.isEmpty(name)) {
                // skip flatten
                return true;
            }
            Iterable iterable = (Iterable) value;
            map.putAll(this.convert(key, iterable));
        } else if (clazz.isArray()) {
            if (StringUtils.isEmpty(name)) {
                // skip flatten
                return true;
            }
            map.putAll(this.convert(key, arrayObjectToList(value)));
        } else if (value instanceof Map) {
            Map m = (Map) value;
            map.putAll(this.convert(key, m));
        } else {
            TypeDescriptor descriptor = (sourceType != null) ? sourceType : TypeDescriptor.forObject(value);
            if (BeanUtils.isSimpleProperty(clazz) || conversionService.canConvert(descriptor, STRING_DESC)) {
                map.put(key, conversionService.convert(value, descriptor, STRING_DESC).toString());
            } else {
                // the value is Java Bean?
                return false;
            }
        }
        // the value has been flatten
        return true;
    }

    /**
     * Convert any array to {@code List<Object>}
     * @param arrayObject array to convert
     * @return list
     */
    private static List<Object> arrayObjectToList(final Object arrayObject) {
        // 'arrayObject' must be array
        return new AbstractList<Object>() {
            @Override
            public Object get(int index) {
                return Array.get(arrayObject, index);
            }

            @Override
            public int size() {
                return Array.getLength(arrayObject);
            }
        };
    }
}