<commit_before>
/*
 * Copyright (C) 2011 <NAME>. All rights reserved.
 *
 * $Id$
 */

package org.dellroad.stuff.vaadin7;

import java.beans.Introspector;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.dellroad.stuff.java.Primitive;

/**
 * Generates a {@link PropertyExtractor} and a list of {@link PropertyDef}s from a class having
 * {@link ProvidesProperty &#64;ProvidesProperty}-annotated methods.
 *
 * @param <T> Java class to be introspected
 */
public class PropertyReader<T> {

    private final ArrayList<PropertyInfo<?>> propertyList = new ArrayList<PropertyInfo<?>>();

    /**
     * Constructor.
     *
     * @param type Java class to be introspected
     * @throws IllegalArgumentException if {@code type} is null
     * @throws IllegalArgumentException if an annotated method with no {@linkplain ProvidesProperty#value property name specified}
     *  has a name which cannot be interpreted as a bean property "getter" method
     * @throws IllegalArgumentException if {@code type} has two {@link ProvidesProperty &#64;ProvidesProperty}-annotated
     *  fields or methods with the same {@linkplain ProvidesProperty#value property name}
     */
    public PropertyReader(Class<T> type) {

        // Sanity check
        if (type == null)
            throw new IllegalArgumentException("null type");

        // Introspect fields and methods
        final TreeMap<String, PropertyInfo<?>> propertyMap = new TreeMap<String, PropertyInfo<?>>();
        for (Class<? super T> klass = type; klass != null; klass = klass.getSuperclass()) {
            for (Field field : klass.getDeclaredFields()) {
                final ProvidesProperty annotation = field.getAnnotation(ProvidesProperty.class);
                if (annotation != null)
                    this.addPropertyInfo(propertyMap, field, annotation.value(), field.getType());
            }
            for (Method method : klass.getDeclaredMethods()) {
                if (method.getReturnType() == void.class || method.getParameterTypes().length > 0)
                    continue;
                final ProvidesProperty annotation = method.getAnnotation(ProvidesProperty.class);
                if (annotation != null) {
                    this.addPropertyInfo(propertyMap, method, annotation.value(), method.getReturnType());
                }
            }
        }
        this.propertyList.addAll(propertyMap.values());
    }

    private void addPropertyInfo(Map<String, PropertyInfo<?>> propertyMap, AccessibleObject member, String name, Class<?> type) {

        // Sanity check type
        if (type == void.class)
            throw new IllegalArgumentException("property `" + name + "' has void type on " + member);

        // Get (non-primitive) type and default value
        final Primitive primitiveType = Primitive.get(type);
        Object defaultValue = null;
        if (type.isPrimitive()) {
            defaultValue = primitiveType.getDefaultValue();
            type = primitiveType.getWrapperType();
        }

        // Get name, if not explicitly specified
        if (name.length() == 0) {
            if (member instanceof Field)
                name = ((Field)member).getName();
            else {
                name = ((Method)member).getName();
                if (name.startsWith("get") && name.length() > 3)
                    name = Introspector.decapitalize(name.substring(3));
                else if (name.startsWith("is") && name.length() > 2 && primitiveType == Primitive.BOOLEAN)
                    name = Introspector.decapitalize(name.substring(2));
                else
                    throw new IllegalArgumentException("can't infer property name from non-getter method " + member);
            }
        }

        // Create new property info
        final PropertyInfo<?> propertyInfo = this.createPropertyInfo(member, name, type, defaultValue);

        // Name already used?
        final PropertyInfo<?> otherPropertyInfo = propertyMap.get(name);
        if (otherPropertyInfo != null) {

            // For overridden methods, the subclass method takes precedence
            if (otherPropertyInfo.getMemberName().equals(propertyInfo.getMemberName()))
                return;

            // Otherwise, there is a duplicate property name
            throw new IllegalArgumentException("duplicate property name `" + name + "' on " + member);
        }

        // Add new property definition
        propertyMap.put(name, propertyInfo);
    }

    // This method exists solely to bind the generic type
    private <V> PropertyInfo<V> createPropertyInfo(AccessibleObject member, String name, Class<V> type, Object defaultValue) {
        return new PropertyInfo<V>(member, name, type, type.cast(defaultValue));
    }

    /**
     * Get a {@link PropertyExtractor} that extracts Vaadin {@link com.vaadin.data.Property} values
     * from instances of this reader's configured class.
     */
    public PropertyExtractor<T> getPropertyExtractor() {
        return new PropertyExtractor<T>() {
            @Override
            public <V> V getPropertyValue(T obj, PropertyDef<V> propertyDef) {
                for (PropertyInfo<?> propertyInfo : PropertyReader.this.propertyList) {<commit_msg>Recurse on interfaces as well as superclass when looking for @ProvidesProperty.<commit_after>
/*
 * Copyright (C) 2011 <NAME>. All rights reserved.
 *
 * $Id$
 */

package org.dellroad.stuff.vaadin7;

import java.beans.Introspector;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.dellroad.stuff.java.Primitive;

/**
 * Generates a {@link PropertyExtractor} and a list of {@link PropertyDef}s from a class having
 * {@link ProvidesProperty &#64;ProvidesProperty}-annotated methods.
 *
 * @param <T> Java class to be introspected
 */
public class PropertyReader<T> {

    private final ArrayList<PropertyInfo<?>> propertyList;

    /**
     * Constructor.
     *
     * @param type Java class to be introspected
     * @throws IllegalArgumentException if {@code type} is null
     * @throws IllegalArgumentException if an annotated method with no {@linkplain ProvidesProperty#value property name specified}
     *  has a name which cannot be interpreted as a bean property "getter" method
     * @throws IllegalArgumentException if {@code type} has two {@link ProvidesProperty &#64;ProvidesProperty}-annotated
     *  fields or methods with the same {@linkplain ProvidesProperty#value property name}
     */
    public PropertyReader(Class<T> type) {

        // Sanity check
        if (type == null)
            throw new IllegalArgumentException("null type");

        // Introspect fields and methods
        final TreeMap<String, PropertyInfo<?>> propertyMap = new TreeMap<String, PropertyInfo<?>>();
        this.findProperties(propertyMap, type);

        // Build property list (sorted by name)
        this.propertyList = new ArrayList<PropertyInfo<?>>(propertyMap.values());
    }

    private void findProperties(Map<String, PropertyInfo<?>> propertyMap, Class<?> klass) {

        // Stop at null
        if (klass == null)
            return;

        // Search fields
        for (Field field : klass.getDeclaredFields()) {
            final ProvidesProperty annotation = field.getAnnotation(ProvidesProperty.class);
            if (annotation != null)
                this.addPropertyInfo(propertyMap, field, annotation.value(), field.getType());
        }

        // Search methods
        for (Method method : klass.getDeclaredMethods()) {
            if (method.getReturnType() == void.class || method.getParameterTypes().length > 0)
                continue;
            final ProvidesProperty annotation = method.getAnnotation(ProvidesProperty.class);
            if (annotation != null)
                this.addPropertyInfo(propertyMap, method, annotation.value(), method.getReturnType());
        }

        // Recurse on interfaces
        for (Class<?> iface : klass.getInterfaces())
            this.findProperties(propertyMap, iface);

        // Recurse on superclass
        this.findProperties(propertyMap, klass.getSuperclass());
    }

    private void addPropertyInfo(Map<String, PropertyInfo<?>> propertyMap, AccessibleObject member, String name, Class<?> type) {

        // Sanity check type
        if (type == void.class)
            throw new IllegalArgumentException("property `" + name + "' has void type on " + member);

        // Get (non-primitive) type and default value
        final Primitive primitiveType = Primitive.get(type);
        Object defaultValue = null;
        if (type.isPrimitive()) {
            defaultValue = primitiveType.getDefaultValue();
            type = primitiveType.getWrapperType();
        }

        // Get name, if not explicitly specified
        if (name.length() == 0) {
            if (member instanceof Field)
                name = ((Field)member).getName();
            else {
                name = ((Method)member).getName();
                if (name.startsWith("get") && name.length() > 3)
                    name = Introspector.decapitalize(name.substring(3));
                else if (name.startsWith("is") && name.length() > 2 && primitiveType == Primitive.BOOLEAN)
                    name = Introspector.decapitalize(name.substring(2));
                else
                    throw new IllegalArgumentException("can't infer property name from non-getter method " + member);
            }
        }

        // Create new property info
        final PropertyInfo<?> propertyInfo = this.createPropertyInfo(member, name, type, defaultValue);

        // Name already used? OK if it's just an overridden member
        final PropertyInfo<?> otherPropertyInfo = propertyMap.get(name);
        if (otherPropertyInfo != null) {

            // For overridden methods, the subclass method takes precedence
            if (otherPropertyInfo.getMemberName().equals(propertyInfo.getMemberName()))
                return;

            // Otherwise, there is a duplicate property name
            throw new IllegalArgumentException("duplicate property name `" + name + "' on " + member);
        }

        // Add new property definition
        propertyMap.put(name, propertyInfo);
    }

    // This method exists solely to bind the generic type
    private <V> PropertyInfo<V> createPropertyInfo(AccessibleObject member, String name, Class<V> type, Object defaultValue) {
        return new PropertyInfo<V>(member, name, type, type.cast(defaultValue));
    }

    /**
     * Get a {@link PropertyExtractor} that extracts Vaadin {@link com.vaadin.data.Property} values
     * from instances of this reader's configured class.
     */
    public PropertyExtractor<T> getPropertyExtractor() {
        return new PropertyExtractor<T>() {
            @Override
            public <V> V getPropertyValue(T obj, PropertyDef<V> propertyDef) {
                for (PropertyInfo<?> propertyInfo : PropertyReader.this.propertyList) {
{'fraction_non_alphanumeric': 0.10441202320267182, 'fraction_numerical': 0.002636667252592723, 'mean_word_length': 2.6424455825864275, 'pattern_counts': {'":': 0, '<': 60, '<?xml version=': 0, '>': 66, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3951989', 'n_tokens_mistral': 2931, 'n_tokens_neox': 2827, 'n_words': 847}