net.femtoparsec.binding.property.BeanPropertyHelper.java Source code

Java tutorial

Introduction

Here is the source code for net.femtoparsec.binding.property.BeanPropertyHelper.java

Source

/*
 * Copyright (C) 2014 Bastien Aracil
 *
 *  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 net.femtoparsec.binding.property;

import net.femtoparsec.binding.property.factory.ChainedBeanPropertyFactory;
import net.femtoparsec.binding.property.factory.DefaultBeanPropertyFactory;
import net.femtoparsec.binding.property.factory.Utils;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.reflect.TypeToken;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.*;

/**
 * Helper for creating the bean property list from a property path;
 *
 * @author Bastien Aracil
 * @version 21/11/2014 - 11:28:10
 */
public class BeanPropertyHelper {

    private static BiMap<Class<?>, Class<?>> PRIMITIVE_TO_BOXED = HashBiMap.create();

    static {
        PRIMITIVE_TO_BOXED.put(boolean.class, Boolean.class);
        PRIMITIVE_TO_BOXED.put(char.class, Character.class);
        PRIMITIVE_TO_BOXED.put(byte.class, Byte.class);
        PRIMITIVE_TO_BOXED.put(short.class, Short.class);
        PRIMITIVE_TO_BOXED.put(int.class, Integer.class);
        PRIMITIVE_TO_BOXED.put(long.class, Long.class);
        PRIMITIVE_TO_BOXED.put(float.class, Float.class);
        PRIMITIVE_TO_BOXED.put(double.class, Double.class);
    }

    /**
     * Soft cache for BeanProperty.
     */
    private static final BeanPropertyCache BEAN_PROPERTY_CACHE = new BeanPropertyCache();

    /**
     * Custom factory to add its own BeanProperty
     */
    public static BeanPropertyFactory CUSTOM_BEAN_FACTORY = null;

    /**
     * The default bean property factory
     */
    private static final BeanPropertyFactory DEFAULT_BEAN_PROPERTY_FACTORY = new DefaultBeanPropertyFactory();

    /**
     * <p>Complete a chained list of bean properties with a extra path.</p>
     *
     * @param parentPath the parent chained list of properties
     * @param path the path to append to the chain
     * @return the new chained list of bean properties
     */
    public static ImmutableList<BeanProperty> completeBeanPropertyList(ImmutableList<BeanProperty> parentPath,
            String path) {
        final List<String> tokens = Arrays.asList(path.split("\\."));
        //get the type of the last token in the parent path and use it to construct the rest of the path
        final TypeToken<?> last = parentPath.get(parentPath.size() - 1).getPropertyType();
        return buildBeanPropertyList(last, tokens, parentPath);
    }

    /**
     * <p>From a property path, create a list of property beans that can be used to query the value point by the property path.</p>
     *
     * <p>For instance, if you have :</p>
     *
     * <ul>
     *     <li>a class <code>Address</code> with the properties :
     *     <ol>
     *         <li><code>city</code> (StringProperty)</li>
     *         <li><code>street</code> (StringProperty)</li>
     *         <li><code>streetNumber</code> (IntProperty)</li>
     *     </ol>
     *     </li>
     *     <li>a class <code>Person</code> with the properties :
     *     <ol>
     *         <li><code>firstName</code> (StringProperty)</li>
     *         <li><code>lastName</code> (StringProperty)</li>
     *         <li><code>address</code> (ObjectProperty&lt;Address&gt;)</li>
     *     </ol>
     *     </li>
     * </ul>
     *
     * <p>You can call this method with <code>Person.class</code> and path="address.city" to get the chained list of bean property to get/set
     * the city of a person. It is preferable to use implementations of {@link net.femtoparsec.binding.PathProperty} directly.</p>
     *
     * @param clazz the class of the root element of the chain
     * @param path the path to the property
     * @return a list of bean properties
     */
    public static ImmutableList<BeanProperty> buildBeanPropertyList(Class<?> clazz, String path) {
        final List<String> tokens = Arrays.asList(path.split("\\."));
        return buildBeanPropertyList(TypeToken.of(clazz), tokens);
    }

    /**
     * <p>From a list of bean properties create a displayable version of the chain.</p>
     * @param beanProperties the bean property list
     * @return a representation of the chained of method used to get the property from the root
     */
    public static String buildPath(Collection<BeanProperty> beanProperties) {
        final StringBuilder sb = new StringBuilder();
        boolean first = true;
        for (BeanProperty beanProperty : beanProperties) {
            if (first) {
                sb.append("(").append(beanProperty.getBeanType().getType()).append(")");
                first = false;
            }
            sb.append(".");
            sb.append(beanProperty.getGetterString());
        }
        return sb.toString();
    }

    /**
     *
     * @param resultTypeToken the type token to check
     * @param requestedResult the requested casting type
     * @return true requestedResult can be used for a {@link net.femtoparsec.binding.PathProperty} that has as <code>resultTypToken</code> as last type token in his property path.
     */
    public static boolean isValidRequestedResult(TypeToken<?> resultTypeToken, Class<?> requestedResult) {
        //we need to check if the resultTypeToken can be cast (converted) to the requestResult
        final Class<?> resultRawType = resultTypeToken.getRawType();
        if (requestedResult.isPrimitive()) {
            return requestedResult == resultRawType;
        } else if (resultTypeToken.isPrimitive()) {
            return isBoxer(resultRawType, requestedResult);
        } else {
            return requestedResult.isAssignableFrom(resultRawType);
        }
    }

    /**
     * Determine if a primitive type can be boxed to a given type
     *
     * @param primitiveToBox the primitive type to check
     * @param boxerType the boxer type to use for the check
     * @return true if the parameter boxerType can be used to box the given primitive type.
     */
    private static boolean isBoxer(Class<?> primitiveToBox, Class<?> boxerType) {
        if (!primitiveToBox.isPrimitive() || boxerType.isPrimitive()) {
            throw new IllegalArgumentException(
                    "'primitiveBox' and 'boxerType' parameters must be primitive and non primitive classes respectively");
        }
        return boxerType.isAssignableFrom(PRIMITIVE_TO_BOXED.get(primitiveToBox));
    }

    /**
     * <p>Create the {@link BeanPropertyFactory} used when creating the list of bean properties. The returned factory
     * will try to use the {@link #CUSTOM_BEAN_FACTORY} first if not null. The custom factory is called first and if it is not able to create the property
     * bean, the default factory will be used.</p>
     *
     * @return the bean property factory to create <code>BeanProperty</code>
     */
    private static BeanPropertyFactory createFactory() {
        return CUSTOM_BEAN_FACTORY == null ? DEFAULT_BEAN_PROPERTY_FACTORY
                : new ChainedBeanPropertyFactory(CUSTOM_BEAN_FACTORY, DEFAULT_BEAN_PROPERTY_FACTORY);
    }

    private static ImmutableList<BeanProperty> buildBeanPropertyList(TypeToken<?> typeToken,
            Collection<String> tokens) {
        return buildBeanPropertyList(typeToken, tokens, ImmutableList.of());
    }

    private static ImmutableList<BeanProperty> buildBeanPropertyList(TypeToken<?> typeToken,
            Collection<String> tokens, ImmutableList<BeanProperty> prefixBeanProperties) {
        final BeanPropertyFactory factory = createFactory();
        final List<BeanProperty> building = new LinkedList<>(prefixBeanProperties);

        TypeToken<?> beanType = typeToken;

        for (String token : tokens) {
            final String propertyName = preparePropertyName(token);

            final Optional<BeanProperty> result;
            final BeanProperty cached = BEAN_PROPERTY_CACHE.getBeanProperty(typeToken, propertyName);

            if (cached != null) {
                result = Optional.of(cached);
            } else if (propertyName.startsWith(BeanProperty.INDEX_PREFIX)) {
                result = factory.createIndexedBeanProperty(beanType, propertyName.substring(1));
            } else {
                result = factory.createBeanProperty(beanType, propertyName);
            }

            final BeanProperty beanProperty = result.orElseThrow(() -> new IllegalArgumentException(
                    "Invalid property : '" + token + "'. Built path : " + buildPath(building)));

            if (cached == null) {
                BEAN_PROPERTY_CACHE.addBeanProperty(beanProperty);
            }
            beanType = beanProperty.getPropertyType();
            building.add(beanProperty);
        }

        return ImmutableList.copyOf(building);

    }

    /**
     * <p>Prepare the property name. Currently, the method simply tests if the property
     * is an integer and if so add the "index prefix" to standardize notation.
     *
     * @param propertyName the property name to prepare
     * @return the prepared property
     */
    private static String preparePropertyName(String propertyName) {
        if (null != Utils.parseInteger(propertyName)) {
            return BeanProperty.INDEX_PREFIX + propertyName;
        }
        return propertyName;
    }

    /**
     *
     * @author Bastien Aracil
     * @version 21/11/2014 - 14:17:09
     */
    public static class BeanPropertyCache {

        private final BiMap<Key, Reference<BeanProperty>> table = HashBiMap.create(200);

        private final ReferenceQueue<BeanProperty> queue = new ReferenceQueue<>();

        private final Thread releaserThread;

        public BeanPropertyCache() {
            this.releaserThread = new Thread(new Runner(), "Bean Property Releaser");
            this.releaserThread.setDaemon(true);
            this.releaserThread.start();
        }

        public synchronized void addBeanProperty(BeanProperty beanProperty) {
            final Reference<BeanProperty> beanPropertyReference = new SoftReference<>(beanProperty, this.queue);
            table.put(new Key(beanProperty), beanPropertyReference);
        }

        public BeanProperty getBeanProperty(TypeToken<?> beanType, String propertyName) {
            final Key key = Key.with(beanType, propertyName);
            final Reference<BeanProperty> value = table.get(Key.with(beanType, propertyName));
            final BeanProperty beanProperty = value == null ? null : value.get();

            if (value != null && beanProperty == null) {
                table.remove(key);
            }

            return beanProperty;
        }

        private class Runner implements Runnable {

            @Override
            public void run() {
                try {
                    Reference<? extends BeanProperty> reference = queue.remove();
                    table.inverse().remove(reference);
                } catch (InterruptedException e) {
                    //quit the runner
                }
            }
        }

        private static class Key {

            private final TypeToken<?> beanType;

            private final String propertyName;

            public Key(TypeToken<?> beanType, String propertyName) {
                this.beanType = beanType;
                this.propertyName = propertyName;
            }

            public Key(BeanProperty beanProperty) {
                this(beanProperty.getBeanType(), beanProperty.getPropertyName());
            }

            @Override
            public boolean equals(Object o) {
                if (this == o)
                    return true;
                if (!(o instanceof Key))
                    return false;

                Key key = (Key) o;

                return beanType.equals(key.beanType) && propertyName.equals(key.propertyName);

            }

            @Override
            public int hashCode() {
                int result = beanType.hashCode();
                result = 31 * result + propertyName.hashCode();
                return result;
            }

            public static Key with(TypeToken<?> beanType, String propertyName) {
                return new Key(beanType, propertyName);
            }
        }
    }
}