net.navatwo.jfxproperties.PropertyObject.java Source code

Java tutorial

Introduction

Here is the source code for net.navatwo.jfxproperties.PropertyObject.java

Source

/*
 * Copyright 2017 jfxproperties contributors
 *
 * 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.navatwo.jfxproperties;

import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.collect.*;
import com.google.common.primitives.Primitives;
import com.google.common.reflect.Invokable;
import com.google.common.reflect.TypeToken;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyLongProperty;
import javafx.beans.property.ReadOnlyProperty;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import jdk.nashorn.internal.ir.annotations.Immutable;
import net.navatwo.jfxproperties.util.PropertyAcceptor;
import net.navatwo.jfxproperties.util.TypeAcceptor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import java.lang.reflect.Field;
import java.util.*;

import static com.google.common.base.Preconditions.*;

/**
 * Gathers all of the properties on a {@link Class} hierarchy except those ignored by {@link IgnoreProperty} and
 * allows for access via {@link PropertyInfo} structures to read and modify the content of the properties.
 *
 * @see IgnoreProperty
 */
@ParametersAreNonnullByDefault
@Immutable
public class PropertyObject<T> {

    /**
     * Base class type
     */
    private final TypeToken<T> base;

    /**
     * Used for {@link #hashCode()} based off of the {@link #base} field.
     */
    private final int hashCode;

    private final Collection<? extends PropertyObject<? super T>> supers;

    private final ImmutableSortedSet<String> ignoredProperties;
    private final ImmutableSortedSet<String> allNames;
    private final ImmutableMap<String, ? extends PropertyInfo<?>> allProperties;

    private final ImmutableSortedSet<String> thisNames;
    private final ImmutableMap<String, ? extends PropertyInfo<?>> localProperties;

    /**
     * Initialize a {@link PropertyObject}.
     * <br/>
     * <p>
     * This should only be called from {@link Builder}.
     *
     * @param fields  Fields found in the hierarchy
     * @param methods Methods found in the hierarchy
     */
    @SuppressWarnings("unchecked")
    PropertyObject(TypeToken<T> base, Collection<? extends PropertyObject<? super T>> supers,
            Set<String> ignoredProperties, Map<String, TypeToken<?>> types, Map<String, Field> fields,
            Map<String, EnumMap<MethodType, Invokable<T, ?>>> methods) {
        this.base = checkNotNull(base, "base == null");
        this.hashCode = base.hashCode();
        this.supers = checkNotNull(supers, "supers == null");

        // Collect all of the properties from the immediate super collectors, these will have been initialized with
        // their super ignored properties as well.
        ImmutableSortedSet.Builder<String> ignoredPropertiesBuilder = ImmutableSortedSet.naturalOrder();
        ignoredPropertiesBuilder.addAll(ignoredProperties);
        supers.stream().flatMap(s -> s.getIgnoredProperties().stream()).forEach(ignoredPropertiesBuilder::add);
        this.ignoredProperties = ignoredPropertiesBuilder.build();

        // now we need to go through and create a mapping of property names to the accessing methods/fields
        // do a union on the keys this gives us all the property names that have been found
        ImmutableMap.Builder<String, PropertyInfo<?>> propertyMapBuilder = ImmutableMap.builder();
        Sets.union(methods.keySet(), fields.keySet()).stream()
                .filter(propName -> !this.ignoredProperties.contains(propName)).forEach(propName -> {
                    // Now build the appropriate PropertyInfo<> implementation dependant on the type of the Field.
                    // We can use the primitive versions when it will be faster for them to execute later.

                    TypeToken<?> propType = types.get(propName).unwrap();

                    EnumMap<MethodType, Invokable<T, ?>> mmap = methods.getOrDefault(propName,
                            new EnumMap<>(MethodType.class));

                    Field field = fields.get(propName);
                    Invokable<T, ? extends ReadOnlyProperty<?>> accessor = (Invokable<T, ? extends ReadOnlyProperty<?>>) mmap
                            .get(MethodType.ACCESSOR);
                    Invokable<T, ?> getter = mmap.get(MethodType.GETTER);
                    Invokable<T, Void> setter = (Invokable<T, Void>) mmap.get(MethodType.SETTER);

                    if (getter != null || setter != null || accessor != null) {

                        PropertyInfoExtractor piExtract = new PropertyInfoExtractor(propName, base, propType, field,
                                getter, setter, accessor);
                        TypeAcceptor.accept(propType, piExtract);

                        propertyMapBuilder.put(propName, piExtract.getPropertyInfo());
                    }
                });

        this.localProperties = propertyMapBuilder.build();
        this.thisNames = ImmutableSortedSet.copyOf(localProperties.keySet());

        supers.stream().flatMap(s -> s.getProperties().entrySet().stream())
                .filter(e -> !this.thisNames.contains(e.getKey()))
                .filter(e -> !this.ignoredProperties.contains(e.getKey()))
                .forEach(e -> propertyMapBuilder.put(e.getKey(), e.getValue()));

        // Now create the immutable structures required and store them.
        allProperties = propertyMapBuilder.build();
        allNames = ImmutableSortedSet.copyOf(allProperties.keySet());
    }

    /**
     * Get the base class for this {@code PropertyObject}.
     *
     * @return Base class
     */
    public Class<T> getBaseClass() {
        return (Class<T>) base.getRawType();
    }

    /**
     * Get the base class for this {@code PropertyObject}.
     *
     * @return Base class
     */
    public TypeToken<T> getBaseType() {
        return base;
    }

    /**
     * Gets a sorted, immutable set of names of properties
     *
     * @return Non-{@code null}, immutable set of property names
     */
    public ImmutableSortedSet<String> getAllNames() {
        return allNames;
    }

    /**
     * Gets a {@link PropertyInfo} instance for the property {@code name}. This allows one to manipulate the
     * property value.
     *
     * @param name         Property name
     * @param propertyType Type of the property
     * @param <V>          Type of the property
     * @return Manipulation for property
     * @throws IllegalArgumentException if the property does not exist or the type expected is not a valid super class
     *                                  of the property's actual type.
     */
    @SuppressWarnings("unchecked")
    public <V> PropertyInfo<V> getProperty(String name, Class<V> propertyType) {
        final PropertyInfo<?> info = allProperties.get(name);
        checkArgument(info != null, "Property %s does not exist on type %s", name, base);
        checkArgument(info.getPropertyType().isSubtypeOf(propertyType),
                "propertyType %s is not assignable from property %s, which is %s", propertyType, name,
                info.getPropertyClass());

        return (PropertyInfo<V>) info;
    }

    /**
     * Gets an {@link IntegerPropertyInfo} instance for the property {@code name}. This allows one to manipulate the
     * property value.
     *
     * @param name Property name
     * @return Manipulation for property
     * @throws IllegalArgumentException if the property does not exist or the type expected is not a valid super class
     *                                  of the property's actual type.
     */
    @SuppressWarnings("unchecked")
    public IntegerPropertyInfo getIntProperty(String name) {

        final PropertyInfo<?> info = allProperties.get(name);
        checkArgument(info != null, "Property %s does not exist on type %s", name, base);
        checkState(Primitives.unwrap(info.getPropertyClass()) == int.class, "Property %s is not an int, it is %s",
                name, info.getPropertyClass());

        return (IntegerPropertyInfo) info;
    }

    /**
     * Gets an {@link LongPropertyInfo} instance for the property {@code name}. This allows one to manipulate the
     * property value.
     *
     * @param name Property name
     * @return Manipulation for property
     * @throws IllegalArgumentException if the property does not exist or the type expected is not a valid super class
     *                                  of the property's actual type.
     */
    @SuppressWarnings("unchecked")
    public LongPropertyInfo getLongProperty(String name) {
        final PropertyInfo<?> info = allProperties.get(name);
        checkArgument(info != null, "Property %s does not exist on type %s", name, base);
        checkState(Primitives.unwrap(info.getPropertyClass()) == long.class, "Property %s is not a long, it is %s",
                name, info.getPropertyClass());

        return (LongPropertyInfo) info;
    }

    /**
     * Gets an {@link DoublePropertyInfo} instance for the property {@code name}. This allows one to manipulate the
     * property value.
     *
     * @param name Property name
     * @return Manipulation for property
     * @throws IllegalArgumentException if the property does not exist or the type expected is not a valid super class
     *                                  of the property's actual type.
     */
    @SuppressWarnings("unchecked")
    public DoublePropertyInfo getDoubleProperty(String name) {
        final PropertyInfo<?> info = allProperties.get(name);
        checkArgument(info != null, "Property %s does not exist on type %s", name, base);
        checkState(Primitives.unwrap(info.getPropertyClass()) == double.class,
                "Property %s is not a double, it is %s", name, info.getPropertyClass());

        return (DoublePropertyInfo) info;
    }

    /**
     * Gets an {@link ListPropertyInfo} instance for the property {@code name}. This allows one to manipulate the
     * property value.
     *
     * @param name Property name
     * @return Manipulation for property
     * @throws IllegalArgumentException if the property does not exist or the type expected is not a valid super class
     *                                  of the property's actual type.
     */
    @SuppressWarnings("unchecked")
    public <V> ListPropertyInfo<V> getListProperty(String name, TypeToken<V> type) {
        final PropertyInfo<?> info = allProperties.get(name);
        checkArgument(info != null, "Property %s does not exist on type %s", name, base);
        checkState(info.getPropertyType().isSubtypeOf(ObservableList.class),
                "Property %s is not a List property, it is %s", name, info.getPropertyClass());

        return (ListPropertyInfo<V>) info;
    }

    /**
     * Gets an {@link ListPropertyInfo} instance for the property {@code name}. This allows one to manipulate the
     * property value.
     * <br/>
     * Delegate to {@link #getListProperty(String, TypeToken)}.
     *
     * @param name Property name
     * @return Manipulation for property
     * @throws IllegalArgumentException if the property does not exist or the type expected is not a valid super class
     *                                  of the property's actual type.
     * @see #getListProperty(String, TypeToken)
     */
    @SuppressWarnings("unchecked")
    public <V> ListPropertyInfo<V> getListProperty(String name, Class<V> type) {
        return getListProperty(name, TypeToken.of(type));
    }

    /**
     * Gets an {@link SetPropertyInfo} instance for the property {@code name}. This allows one to manipulate the
     * property value.
     *
     * @param name Property name
     * @return Manipulation for property
     * @throws IllegalArgumentException if the property does not exist or the type expected is not a valid super class
     *                                  of the property's actual type.
     */
    @SuppressWarnings("unchecked")
    public <V> SetPropertyInfo<V> getSetProperty(String name, TypeToken<V> type) {
        final PropertyInfo<?> info = allProperties.get(name);
        checkArgument(info != null, "Property %s does not exist on type %s", name, base);
        checkState(info.getPropertyType().isSubtypeOf(ObservableSet.class),
                "Property %s is not a Set property, it is %s", name, info.getPropertyClass());

        final SetPropertyInfo<V> setInfo = (SetPropertyInfo<V>) info;

        checkState(setInfo.getElementType().isSubtypeOf(type),
                "Invalid element type, actual {} property type is {}", name, info.getPropertyType());

        return setInfo;
    }

    public PropertyInfo<?> getProperty(String name, PropertyAcceptor acceptor) {
        checkNotNull(acceptor, "acceptor == null");

        PropertyInfo<?> info = allProperties.get(name);
        checkArgument(info != null, "Property %s does not exist on type %s", name, base);

        PropertyAcceptor.accept(info, acceptor);

        return info;
    }

    /**
     * Gets a mapping of all {@link PropertyInfo} values keyed by {@link PropertyInfo#getName()}.
     *
     * @return Non-{@code null}, possibly empty set of properties
     */
    public ImmutableMap<String, ? extends PropertyInfo<?>> getProperties() {
        return allProperties;
    }

    /**
     * Gets a mapping of all {@link PropertyInfo} values keyed by {@link PropertyInfo#getName()}.
     *
     * @return Non-{@code null}, possibly empty set of properties
     */
    public ImmutableMap<String, ? extends PropertyInfo<?>> getLocalProperties() {
        return localProperties;
    }

    /**
     * Gets the {@link PropertyInfo#getName() names} of the properties that were found but ignored.
     *
     * @return Non-{@code null}, but possibly empty set of names
     */
    public ImmutableSortedSet<String> getIgnoredProperties() {
        return ignoredProperties;
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof PropertyObject)) {
            return false;
        }
        final PropertyObject<?> that = (PropertyObject<?>) o;
        return Objects.equal(base, that.base);
    }

    @Override
    public int hashCode() {
        return hashCode;
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this).addValue(base).toString();
    }

    /**
     * Defines the different method types known for building Property objects
     */
    enum MethodType {
        /**
         * Method that returns an instance of the value of a property
         */
        GETTER,

        /**
         * Method that sets the instance of the value of a property
         */
        SETTER,

        /**
         * Accesses a property directly
         */
        ACCESSOR,

        /**
         * Unknown method found
         */
        NONE
    }

    /**
     * Creates a new {@link Builder} instance via the convenience method.
     *
     * @return New instance
     * @see Builder
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Implements a Builder for creating {@link PropertyObject} values. This class allows for adjusting
     * how fields/methods are reflected.
     */
    @SuppressWarnings("WeakerAccess")
    public static class Builder {

        private static final Logger logger = LogManager.getLogger(Builder.class);

        private final ImmutableSet<String> fieldPrefixes;
        static final ImmutableSet<String> DEFAULT_FIELD_PREFIXES = ImmutableSet.of("");

        private final ImmutableSet<String> propertySuffixes;
        static final ImmutableSet<String> DEFAULT_PROPERTY_SUFFIXES = ImmutableSet.of("Property");

        private final ImmutableSet<String> getterPrefixes;
        static final ImmutableSet<String> DEFAULT_GETTER_PREFIXES = ImmutableSet.of("get", "is");

        private final ImmutableSet<String> setterPrefixes;
        static final ImmutableSet<String> DEFAULT_SETTER_PREFIXES = ImmutableSet.of("set");

        private final ImmutableMap.Builder<TypeToken<?>, PropertyObject<?>> cacheBuilder;

        Builder() {
            this(Collections.emptyMap(),
                    // string info
                    ImmutableSet.of(), ImmutableSet.of(), ImmutableSet.of(), ImmutableSet.of());
        }

        Builder(Map<TypeToken<?>, PropertyObject<?>> cache, Set<String> fieldPrefixes, Set<String> propertySuffixes,
                Set<String> getterPrefixes, Set<String> setterPrefixes) {
            this.cacheBuilder = ImmutableMap.builder();
            cacheBuilder.putAll(checkNotNull(cache, "cache == null"));

            // handle default prefix/suffixes

            if (propertySuffixes.isEmpty()) {
                this.propertySuffixes = DEFAULT_PROPERTY_SUFFIXES;
            } else {
                this.propertySuffixes = ImmutableSet.copyOf(propertySuffixes);
            }

            if (fieldPrefixes.isEmpty()) {
                this.fieldPrefixes = DEFAULT_FIELD_PREFIXES;
            } else {
                this.fieldPrefixes = ImmutableSet.copyOf(fieldPrefixes);
            }

            if (getterPrefixes.isEmpty()) {
                this.getterPrefixes = DEFAULT_GETTER_PREFIXES;
            } else {
                this.getterPrefixes = ImmutableSet.copyOf(getterPrefixes);
            }

            if (setterPrefixes.isEmpty()) {
                this.setterPrefixes = DEFAULT_SETTER_PREFIXES;
            } else {
                this.setterPrefixes = ImmutableSet.copyOf(setterPrefixes);
            }

        }

        /**
         * Using the stored values, this traverses the class hierarchy collecting and filtering
         * "properties."
         *
         * @return New {@code PropertyObject} instance
         */
        public <T> ImmutableMap<Class<?>, PropertyObject<?>> buildAll(TypeToken<T> type) {
            return buildAll(type, ImmutableMap.of());
        }

        /**
         * Using the stored values, this traverses the class hierarchy collecting and filtering
         * "properties."
         *
         * @param type  The base of the collection
         * @param cache Known classes used to speed up collection
         * @return New {@code PropertyObject} instance
         */
        public <T> ImmutableMap<Class<?>, PropertyObject<?>> buildAll(TypeToken<T> type,
                ImmutableMap<Class<?>, PropertyObject<?>> cache) {
            logger.traceEntry("PropertyObject.Builder#build(type: {}, cache: {} entries)", type, cache.size());

            return logger.traceExit(new RealPropertyObjectBuilder<>(type, this).build(cache));
        }

        /**
         * Using the stored values, this traverses the class hierarchy collecting and filtering
         * "properties."
         *
         * @return New {@code PropertyObject} instance
         */
        public <T> PropertyObject<T> build(TypeToken<T> type) {
            logger.traceEntry("PropertyObject.Builder#build({})", type);

            Map<Class<?>, PropertyObject<?>> instanceMap = buildAll(type);
            @SuppressWarnings("unchecked")
            PropertyObject<T> obj = (PropertyObject<T>) instanceMap.get(type.getRawType());
            checkState(obj != null, "Could get PropertyObject for base type {}", type);
            return logger.traceExit(obj);
        }

        /**
         * Using the stored values, this traverses the class hierarchy collecting and filtering
         * "properties."
         *
         * @return New {@code PropertyObject} instance
         */
        public <T> PropertyObject<T> build(Class<T> clazz) {
            logger.traceEntry("PropertyObject.Builder#build({})", clazz);

            return logger.traceExit(build(TypeToken.of(clazz)));
        }

        /**
         * Removes the prefix string from the string if it is present.
         *
         * @param value  String modified
         * @param prefix Prefix removed
         * @return String with the prefix removed, or the original string
         */
        static String removePrefix(String value, String prefix) {
            if (!prefix.isEmpty() && value.startsWith(prefix) && prefix.length() != value.length()) { // method called "get" for example...
                return Character.toLowerCase(value.charAt(prefix.length())) + value.substring(prefix.length() + 1);
            } else {
                return value;
            }
        }

        /**
         * Checks if the passed value matches the prefix.
         *
         * @param value  Value to check
         * @param prefix Prefix removed
         * @return {@code suffix} if a prefix is found, {@link Optional#empty()} if not
         * @see #matchesPrefix(String, Iterable)
         */
        static Optional<String> matchesPrefix(String value, String... prefix) {
            return Arrays.stream(prefix).filter(pre -> value.length() > pre.length()).filter(value::startsWith)
                    .findFirst();
        }

        /**
         * Checks if the passed value matches the prefix.
         *
         * @param value    Value to check
         * @param prefixes Prefix values to check
         * @return {@code suffix} if a prefix is found, {@link Optional#empty()} if not
         */
        static Optional<String> matchesPrefix(String value, Iterable<String> prefixes) {
            return Streams.stream(prefixes).filter(pre -> value.length() > pre.length()).filter(value::startsWith)
                    .findFirst();
        }

        /**
         * Removes the suffix string from the string if it is present.
         *
         * @param value  String modified
         * @param suffix Suffix removed
         * @return String with the suffix removed, or the original string
         */
        static String removeSuffix(String value, String suffix) {
            if (!value.isEmpty() && value.endsWith(suffix) && suffix.length() != value.length()) {
                return value.substring(0, value.length() - suffix.length());
            }

            return value;
        }

        /**
         * Checks if the passed value matches the prefix.
         *
         * @param value  Value to check
         * @param suffix Suffix searched for
         * @return {@code suffix} if a suffix is found, {@link Optional#empty()} if not
         * @see #matchesSuffix(String, Iterable)
         */
        static Optional<String> matchesSuffix(String value, String... suffix) {
            return matchesSuffix(value, ImmutableList.copyOf(suffix));
        }

        /**
         * Checks if the passed value matches the prefix.
         *
         * @param value    Value to check
         * @param suffixes Suffixes to search for
         * @return {@code suffix} if a suffix is found, {@link Optional#empty()} if not
         */
        static Optional<String> matchesSuffix(String value, Iterable<String> suffixes) {
            return Streams.stream(suffixes).filter(suff -> value.length() > suff.length()).filter(value::endsWith)
                    .findFirst();
        }

        /**
         * Get the prefix used to discern if something is a property cared about.
         * <br/>
         * Defaults to {@link #DEFAULT_FIELD_PREFIXES}
         *
         * @return Prefix searched for
         */
        ImmutableSet<String> getFieldPrefixes() {
            return fieldPrefixes;
        }

        /**
         * Sets the {@link #getFieldPrefixes() field prefix} to the passed value.
         *
         * @param fieldPrefixes New value for field prefix
         * @return New {@link Builder} instance
         * @see #getFieldPrefixes()
         * @see #setFieldPrefixes(Iterable)
         */
        public Builder setFieldPrefixes(final String... fieldPrefixes) {
            return setFieldPrefixes(ImmutableSet.copyOf(fieldPrefixes));
        }

        /**
         * Sets the {@link #getFieldPrefixes() field prefix} to the passed value.
         *
         * @param fieldPrefixes New value for field prefix
         * @return New {@link Builder} instance
         * @see #getFieldPrefixes()
         * @see #setFieldPrefixes(String[])
         */
        public Builder setFieldPrefixes(final Iterable<String> fieldPrefixes) {
            return new Builder(cacheBuilder.build(), ImmutableSet.copyOf(fieldPrefixes), propertySuffixes,
                    getterPrefixes, setterPrefixes);
        }

        ImmutableSet<String> getPropertySuffixes() {
            return propertySuffixes;
        }

        /**
         * Sets the {@link #getPropertySuffixes() property suffix} to the passed value.
         *
         * @param propertySuffixes New value for property suffix
         * @return New {@link Builder} instance
         * @see #getPropertySuffixes()
         * @see #setPropertySuffixes(Iterable)
         */
        public Builder setPropertySuffixes(final String... propertySuffixes) {
            return setPropertySuffixes(ImmutableSet.copyOf(propertySuffixes));
        }

        /**
         * Sets the {@link #getPropertySuffixes() property suffix} to the passed value.
         *
         * @param propertySuffixes New value for property suffix
         * @return New {@link Builder} instance
         * @see #getPropertySuffixes()
         * @see #setPropertySuffixes(String...)
         */
        public Builder setPropertySuffixes(final Iterable<String> propertySuffixes) {
            return new Builder(cacheBuilder.build(), fieldPrefixes, ImmutableSet.copyOf(propertySuffixes),
                    getterPrefixes, setterPrefixes);
        }

        /**
         * The prefix used when deciding if a method is a "getter".
         * <br/>
         * Defaults to: {@link #DEFAULT_GETTER_PREFIXES}
         *
         * @return Current getter prefix
         */
        ImmutableSet<String> getGetterPrefixes() {
            return getterPrefixes;
        }

        /**
         * Sets the {@link #getGetterPrefixes() getter prefix} to the passed value.
         *
         * @param getterPrefixes New value for getter prefix
         * @return New {@link Builder} instance
         * @see #getGetterPrefixes()
         */
        public Builder setGetterPrefixes(final String... getterPrefixes) {
            return setGetterPrefixes(ImmutableSet.copyOf(getterPrefixes));
        }

        /**
         * Sets the {@link #getGetterPrefixes() getter prefix} to the passed value.
         *
         * @param getterPrefixes New value for getter prefix
         * @return New {@link Builder} instance
         * @see #getGetterPrefixes()
         */
        public Builder setGetterPrefixes(final Iterable<String> getterPrefixes) {
            return new Builder(cacheBuilder.build(), fieldPrefixes, propertySuffixes,
                    ImmutableSet.copyOf(getterPrefixes), setterPrefixes);
        }

        /**
         * The prefix used when deciding if a method is a "setter".
         * <br/>
         * Defaults to: {@link #DEFAULT_SETTER_PREFIXES}
         *
         * @return Current setter prefix
         */
        ImmutableSet<String> getSetterPrefixes() {
            return setterPrefixes;
        }

        /**
         * Sets the {@link #getGetterPrefixes() setter prefix} to the passed value.
         *
         * @param setterPrefixes New value for setter prefix
         * @return New {@link Builder} instance
         * @see #getSetterPrefixes()
         */
        public Builder setSetterPrefixes(final String... setterPrefixes) {
            return setSetterPrefixes(ImmutableSet.copyOf(setterPrefixes));
        }

        /**
         * Sets the {@link #getGetterPrefixes() setter prefix} to the passed value.
         *
         * @param setterPrefixes New value for setter prefix
         * @return New {@link Builder} instance
         * @see #getSetterPrefixes()
         */
        public Builder setSetterPrefixes(final Iterable<String> setterPrefixes) {
            return new Builder(cacheBuilder.build(), fieldPrefixes, propertySuffixes, getterPrefixes,
                    ImmutableSet.copyOf(setterPrefixes));
        }

        /**
         * Adds all of the entries in the Map into the current cache entries.
         *
         * @param entries Entries to add into the known cache
         * @return <strong>Original instance, {@code this}.</strong>
         */
        public Builder withCacheEntries(final Map<TypeToken<?>, PropertyObject<?>> entries) {
            cacheBuilder.putAll(entries);
            return this;
        }
    }

    /**
     * Uses the type information to discern and pull out what type of {@link PropertyInfo} should be used as the
     * base for the state passed. This is stored in the {@link #getPropertyInfo()} return result
     */
    @SuppressWarnings("unchecked")
    private class PropertyInfoExtractor implements TypeAcceptor {
        private final TypeToken<T> base;
        private final String propName;
        private final Field field;
        private final Invokable<T, ?> getter;
        private final Invokable<T, Void> setter;
        private final Invokable<T, ? extends ReadOnlyProperty<?>> accessor;
        private final TypeToken<?> propType;

        private PropertyInfo<?> pi;

        PropertyInfoExtractor(String propName, TypeToken<T> base, TypeToken<?> propType, @Nullable Field field,
                @Nullable Invokable<T, ?> getter, @Nullable Invokable<T, Void> setter,
                @Nullable Invokable<T, ? extends ReadOnlyProperty<?>> accessor) {
            this.base = base;
            this.propName = propName;
            this.field = field;
            this.getter = getter;
            this.setter = setter;
            this.accessor = accessor;
            this.propType = propType;

            pi = null;
        }

        @Override
        public void acceptInt() {
            pi = new IntegerPropertyInfo(base, propName, field, (Invokable<Object, Integer>) getter,
                    (Invokable<Object, Void>) setter,
                    (Invokable<Object, ? extends ReadOnlyIntegerProperty>) accessor);
        }

        @Override
        public void acceptLong() {
            pi = new LongPropertyInfo(base, propName, field, (Invokable<Object, Long>) getter,
                    (Invokable<Object, Void>) setter, (Invokable<Object, ? extends ReadOnlyLongProperty>) accessor);
        }

        @Override
        public void acceptDouble() {
            pi = new DoublePropertyInfo(base, propName, field, (Invokable<Object, Double>) getter,
                    (Invokable<Object, Void>) setter,
                    (Invokable<Object, ? extends ReadOnlyDoubleProperty>) accessor);
        }

        @Override
        public void acceptObject(TypeToken<?> type) {
            pi = new ObjectPropertyInfo<>(base, propName, (TypeToken<Object>) propType, field,
                    (Invokable<Object, Object>) getter, (Invokable<Object, Void>) setter,
                    (Invokable<Object, ? extends ReadOnlyProperty<Object>>) accessor);
        }

        @Override
        public void acceptList(TypeToken<?> elementType) {
            pi = new ListPropertyInfo<>(base, propName, (TypeToken<List<Object>>) propType,
                    (TypeToken<Object>) elementType, field, (Invokable<Object, List<Object>>) getter,
                    (Invokable<Object, Void>) setter,
                    (Invokable<Object, ? extends ReadOnlyProperty<List<Object>>>) accessor);
        }

        @Override
        public void acceptSet(TypeToken<?> elementType) {
            pi = new SetPropertyInfo<>(base, propName, (TypeToken<Set<Object>>) propType,
                    (TypeToken<Object>) elementType, field, (Invokable<Object, Set<Object>>) getter,
                    (Invokable<Object, Void>) setter,
                    (Invokable<Object, ? extends ReadOnlyProperty<Set<Object>>>) accessor);
        }

        @Override
        public void acceptMap(TypeToken<?> keyType, TypeToken<?> valueType) {
            pi = new MapPropertyInfo<>(base, propName, (TypeToken<Map<Object, Object>>) propType,
                    (TypeToken<Object>) keyType, (TypeToken<Object>) valueType, field,
                    (Invokable<Object, Map<Object, Object>>) getter, (Invokable<Object, Void>) setter,
                    (Invokable<Object, ? extends ReadOnlyProperty<Map<Object, Object>>>) accessor);
        }

        /**
         * Gets the underlying {@link PropertyInfo} that was found.
         *
         * @return Found property info
         * @throws IllegalStateException if this has not been used with
         *                               {@link TypeAcceptor#accept(TypeToken, TypeAcceptor)} yet
         */
        PropertyInfo<?> getPropertyInfo() {
            checkState(pi != null, "Must use this type with TypeAcceptor#accept() before calling this method");
            return pi;
        }
    }
}