All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.minimalcode.reflect.Bean Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2015 Fabio Piro (minimalcode.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.minimalcode.reflect;

import java.lang.ref.SoftReference;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * A bean provides introspected information about the properties
 * of a {@link Class} or an interface.
 *
 * @author Fabio Piro
 * @see Property
 * @see Class#getFields()
 * @see Class#getMethods()
 * @see Class#getDeclaredFields()
 * @see Class#getDeclaredMethods()
 */
public final class Bean {
    private final static Map, SoftReference>> beansCache = new ConcurrentHashMap, SoftReference>>();

    private final Class type;
    private final Map properties;
    private final Map declaredProperties;

    /**
     * Introspects a {@link Class} or an interface and learns about all
     * its {@link Property} elements.
     *
     * 

If the target type has been previously analized then the {@link Bean} * instance is retrieved from a thread-safe {@link SoftReference} cache. * * @param beanClass the class or interface to analize * @return a {@link Bean} object describing the target class or interface * @throws NullPointerException if the given beanClass parameter is {@code null} */ @SuppressWarnings("unchecked") public static Bean forClass(Class beanClass) { if (beanClass == null) { throw new NullPointerException("Cannot instrospect a bean with a 'null' beanClass."); } Bean bean; SoftReference> softReference = beansCache.get(beanClass); if (softReference == null) { bean = new Bean(beanClass); beansCache.put(beanClass, new SoftReference>(bean)); } else { bean = softReference.get(); if (bean == null) { bean = new Bean(beanClass); beansCache.put(beanClass, new SoftReference>(bean)); } } return bean; } /** * Internal: Introspects a {@link Class} or an interface and learns about all * its {@link Property} elements. * *

Private constructor, as the only way to get a {@link Bean} object is through * the {@link #forClass(Class)} static factory method. * * @param type the class or interface to analize */ private Bean(Class type) { this.type = type; // Properties Map properties = new HashMap(); for(PropertyDescriptor descriptor : getPropertyDescriptors(type.getMethods())) { Property property = new Property(this, descriptor.name, descriptor.readMethod, descriptor.writeMethod); properties.put(property.getName(), property); } // Declared Properties Map declaredProperties = new HashMap(); for(PropertyDescriptor descriptor : getPropertyDescriptors(type.getDeclaredMethods())) { Property declaredProperty = new Property(this, descriptor.name, descriptor.readMethod, descriptor.writeMethod); // Properties are immutable and Map::containsValue(V) uses Property::equals() to check equality. // Considering that a property with the same name, bean and accessors is "technically" equivalent // to its corrispective declared version, then it can be used instead, saving some heap memory space. if(properties.containsValue(declaredProperty)) { declaredProperties.put(declaredProperty.getName(), properties.get(declaredProperty.getName())); } else { declaredProperties.put(declaredProperty.getName(), declaredProperty); } } this.properties = optimizeMap(properties); this.declaredProperties = optimizeMap(declaredProperties); } /** * Internal: Introspects the given type and creates the appropriate {@link PropertyDescriptor}. * *

This logic is a completely rewritten version of {@link java.beans.Introspector}, * providing fewer bounds on the matching types (less strict equivalence for the return * type of readMethod and the first parameter of writeMethod) and a supplementary support * for declared accessors. In addition to this, the new implementation was needed as the * package {@code java.beans} is absent from the Android's Java API implementation. * * @param methods the methods to parse * @return all the property descriptors found */ private static Collection getPropertyDescriptors(Method[] methods) { List desciptorsHolder = new ArrayList(); // Collects writeMetod and readMethod for (Method method : methods) { if (isStatic(method) || method.isSynthetic()) { continue; } String name = method.getName(); if (method.getParameterTypes().length == 0) { // Getter if (name.length() > 3 && name.startsWith("get") && method.getReturnType() != void.class) { PropertyDescriptor info = new PropertyDescriptor(); info.name = uncapitalize(name.substring(3)); info.readMethod = method; info.isGetter = true; desciptorsHolder.add(info); } // Isser else if (name.length() > 2 && name.startsWith("is") && method.getReturnType() == boolean.class) { PropertyDescriptor info = new PropertyDescriptor(); info.name = uncapitalize(name.substring(2)); info.readMethod = method; info.isIsser = true; desciptorsHolder.add(info); } } else if (method.getParameterTypes().length == 1) { // Setter if (name.length() > 3 && name.startsWith("set") && method.getReturnType() == void.class) { PropertyDescriptor info = new PropertyDescriptor(); info.name = uncapitalize(name.substring(3)); info.writeMethod = method; info.isSetter = true; desciptorsHolder.add(info); } } } // Merges descriptors with the same name into a single entity Map descriptors = new HashMap(); for (PropertyDescriptor descriptor : desciptorsHolder) { PropertyDescriptor instance = descriptors.get(descriptor.name); if (instance == null) { descriptors.put(descriptor.name, descriptor); instance = descriptor; } if (descriptor.isIsser) { instance.readMethod = descriptor.readMethod; } else if (descriptor.isGetter) { // if both getter and isser methods are present as descriptors, // the isser is chose as readMethod, and the getter discarded if (instance.readMethod == null) { instance.readMethod = descriptor.readMethod; } } else if (descriptor.isSetter) { instance.writeMethod = descriptor.writeMethod; } } return descriptors.values(); } /** * Internal: Converts a string to the normal Java variable * name capitalization. * *

This normally means converting the first character * from upper case to lower case, but in the (unusual) special * case when there is more than one character and both the first * and second characters are upper case, the string is returned * immutate. * *

Thus "SomeVariable" becomes "someVariable" and "X" becomes * "x", but "URL" stays as "URL". * * @param name The string to be uncapitolized * @return The uncapitolized version of the string * @see java.beans.Introspector#decapitalize(String) */ private static String uncapitalize(String name) { if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && Character.isUpperCase(name.charAt(0))){ return name; } char chars[] = name.toCharArray(); chars[0] = Character.toLowerCase(chars[0]); return new String(chars); } /** * Internal: Shortcut for {@link Modifier#isPrivate(int)} (int)}, {@code null}-safe. * * @param member the member to check * @return true if the member is private, else false */ static boolean isPrivate(Member member) { return (member != null) && Modifier.isPrivate(member.getModifiers()); } /** * Internal: Shortcut for {@link Modifier#isPublic(int)}, {@code null}-safe. * * @param member the member to check * @return true if the member is public, else false */ static boolean isPublic(Member member) { return (member != null) && Modifier.isPublic(member.getModifiers()); } /** * Internal: Shortcut for {@link Modifier#isStatic(int)}, {@code null}-safe. * * @param member the member to check * @return true if the member is static, else false */ static boolean isStatic(Member member) { return (member != null) && Modifier.isStatic(member.getModifiers()); } /** * Returns the type of the analized class or interface. * * @return the analized class or interface */ public Class getType() { return type; } /** * Returns a single public property of the analized class or interface, * if present, else {@code null}. * * @param propertyName the name of the property to retrieve * @return the {@link Property} searched, or {@code null} if not found * @throws NullPointerException if propertyName parameter is {@code null} */ public Property getProperty(String propertyName) { if (propertyName == null) { throw new NullPointerException("Cannot get a property with a 'null' propertyName."); } return properties.get(propertyName); } /** * Returns a single declared property of the analized class or interface, * if present, else {@code null}. * * @param propertyName the name of the declared property to retrieve * @return the declared {@link Property} searched, or {@code null} if not found * @throws NullPointerException if propertyName parameter is {@code null} */ public Property getDeclaredProperty(String propertyName) { if (propertyName == null) { throw new NullPointerException("Cannot get a declared property with a 'null' propertyName."); } return declaredProperties.get(propertyName); } /** * Returns an array containing {@link Property} objects reflecting all the * public properties of the analized class or interface. * *

The array includes those properties whose public {@link Method} accessors * are declared in the analized class or interface, or inherited from its * superclasses and superinterfaces. * *

The caller of this method is free to modify the returned array; it will * have no effect on the arrays returned to other callers. * *

The elements in the returned array are not sorted and are not in any * particular order. * * @return an array with a shallow copy of all the public properties of this bean */ public Property[] getProperties() { return properties.values().toArray(new Property[properties.size()]); } /** * Returns an array containing {@link Property} objects reflecting all the * declared properties of the class or interface. * *

The array will include properties whose {@link Method} accessors are * public, protected, default (package) access, and private, but exclude * inherited accessors. * *

The caller of this method is free to modify the returned array; it will * have no effect on the arrays returned to other callers. * *

The elements in the returned array are not sorted and are not in any * particular order. * * @return an array with a shallow copy of all the declared properties of this bean */ public Property[] getDeclaredProperties() { return declaredProperties.values().toArray(new Property[declaredProperties.size()]); } /** * Compares this {@code Bean} against the specified object. * *

Returns true if the objects are the same. Two {@code Bean} are the * same if they analized the same class or interface. * * @param obj the object to compare * @return true if the objects are the same */ @Override public boolean equals(Object obj) { if (obj == this) return true; if (obj == null || getClass() != obj.getClass()) return false; Bean other = (Bean) obj; return type.equals(other.type); } /** * Returns a hashcode for this {@code Bean}. * * @return this bean's hashcode */ @Override public int hashCode() { return type.getName().hashCode(); } /** * Returns a textual rapresentation of this bean. * *

The string representation is the string "bean", * followed by a space, and then by the fully qualified name of * the type analized in the format returned by {@code getName}. * * @return a string representation of this bean object */ @Override public String toString() { return "bean " + type.getName(); } /** * Internal: Returns an optimized (memory-wise) version of a map. * *

{@link Collections.EmptyMap} has near-zero memory * allocation, while {@link Collections.SingletonMap} has * a faster {@link Map#get(Object)} lookup for maps with only a * single entry. * * @param map the map to check * @return the optimized {@link Map} (could be immutable) */ static Map optimizeMap(Map map) { if (map.isEmpty()) { return Collections.emptyMap(); } else if (map.size() == 1) { Map.Entry entry = map.entrySet().iterator().next(); return Collections.singletonMap(entry.getKey(), entry.getValue()); } else { return map; } } /** * Internal: Custom dataholder. */ private final static class PropertyDescriptor { public String name; public Method readMethod; public Method writeMethod; public boolean isIsser; public boolean isGetter; public boolean isSetter; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy