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

com.vaadin.ui.declarative.FieldBinder Maven / Gradle / Ivy

There is a newer version: 8.27.3
Show newest version
/*
 * Copyright (C) 2000-2024 Vaadin Ltd
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See  for the full
 * license.
 */
package com.vaadin.ui.declarative;

import java.beans.IntrospectionException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Logger;

import com.vaadin.ui.Component;

/**
 * Binder utility that binds member fields of a design class instance to given
 * component instances. Only fields of type {@link Component} are bound
 *
 * @since 7.4
 * @author Vaadin Ltd
 */
public class FieldBinder implements Serializable {

    // the instance containing the bound fields
    private Object bindTarget;
    // mapping between field names and Fields
    private Map fieldMap = new HashMap<>();

    /**
     * Creates a new instance of LayoutFieldBinder.
     *
     * @param design
     *            the design class instance containing the fields to bind
     * @throws IntrospectionException
     *             if the given design class can not be introspected
     */
    public FieldBinder(Object design) throws IntrospectionException {
        this(design, design.getClass());
    }

    /**
     * Creates a new instance of LayoutFieldBinder.
     *
     * @param design
     *            the instance containing the fields
     * @param classWithFields
     *            the class which defines the fields to bind
     * @throws IntrospectionException
     *             if the given design class can not be introspected
     */
    public FieldBinder(Object design, Class classWithFields)
            throws IntrospectionException {
        if (design == null) {
            throw new IllegalArgumentException("The design must not be null");
        }
        bindTarget = design;
        resolveFields(classWithFields);
    }

    /**
     * Returns a collection of field names that are not bound.
     *
     * @return a collection of fields assignable to Component that are not bound
     */
    public Collection getUnboundFields() throws FieldBindingException {
        List unboundFields = new ArrayList<>();
        for (Field f : fieldMap.values()) {
            try {
                Object value = getFieldValue(bindTarget, f);
                if (value == null) {
                    unboundFields.add(f.getName());
                }
            } catch (IllegalArgumentException | IllegalAccessException e) {
                throw new FieldBindingException("Could not get field value", e);
            }
        }
        if (!unboundFields.isEmpty()) {
            getLogger().severe(
                    "Found unbound fields in component root :" + unboundFields);
        }
        return unboundFields;
    }

    /**
     * Resolves the fields of the design class instance.
     */
    private void resolveFields(Class classWithFields) {
        for (Field memberField : getFields(classWithFields)) {
            if (Component.class.isAssignableFrom(memberField.getType())) {
                fieldMap.put(memberField.getName().toLowerCase(Locale.ROOT),
                        memberField);
            }
        }
    }

    /**
     * Tries to bind the given {@link Component} instance to a member field of
     * the bind target. The name of the bound field is constructed based on the
     * id or caption of the instance, depending on which one is defined. If a
     * field is already bound (not null), {@link FieldBindingException} is
     * thrown.
     *
     * @param instance
     *            the instance to be bound to a field
     * @return true on success, otherwise false
     * @throws FieldBindingException
     *             if error occurs when trying to bind the instance to a field
     */
    public boolean bindField(Component instance) {
        return bindField(instance, null);
    }

    /**
     * Tries to bind the given {@link Component} instance to a member field of
     * the bind target. The fields are matched based on localId, id and caption.
     *
     * @param instance
     *            the instance to be bound to a field
     * @param localId
     *            the localId used for mapping the field to an instance field
     * @return true on success
     * @throws FieldBindingException
     *             if error occurs when trying to bind the instance to a field
     */
    public boolean bindField(Component instance, String localId) {
        // check that the field exists, is correct type and is null
        boolean success = bindFieldByIdentifier(localId, instance);
        if (!success) {
            success = bindFieldByIdentifier(instance.getId(), instance);
        }
        if (!success) {
            success = bindFieldByIdentifier(instance.getCaption(), instance);
        }
        if (!success) {
            String idInfo = "localId: " + localId + " id: " + instance.getId()
                    + " caption: " + instance.getCaption();
            getLogger().finest("Could not bind component to a field "
                    + instance.getClass().getName() + " " + idInfo);
        }
        return success;
    }

    /**
     * Tries to bind the given {@link Component} instance to a member field of
     * the bind target. The field is matched based on the given identifier. If a
     * field is already bound (not null), {@link FieldBindingException} is
     * thrown.
     *
     * @param identifier
     *            the identifier for the field.
     * @param instance
     *            the instance to be bound to a field
     * @return true on success
     * @throws FieldBindingException
     *             if error occurs when trying to bind the instance to a field
     */
    private boolean bindFieldByIdentifier(String identifier,
            Component instance) {
        try {
            // create and validate field name
            String fieldName = asFieldName(identifier);
            if (fieldName.isEmpty()) {
                return false;
            }
            // validate that the field can be found
            Field field = fieldMap.get(fieldName.toLowerCase(Locale.ROOT));
            if (field == null) {
                getLogger()
                        .fine("No field was found by identifier " + identifier);
                return false;
            }
            // validate that the field is not set
            Object fieldValue = getFieldValue(bindTarget, field);
            if (fieldValue != null) {
                getLogger().fine("The field \"" + fieldName
                        + "\" was already mapped. Ignoring.");
            } else {
                // set the field value
                field.set(bindTarget, instance);
            }
            return true;
        } catch (IllegalAccessException | IllegalArgumentException e) {
            throw new FieldBindingException(
                    "Field binding failed for " + identifier, e);
        }
    }

    private Object getFieldValue(Object object, Field field)
            throws IllegalArgumentException, IllegalAccessException {
        if (!field.isAccessible()) {
            field.setAccessible(true);
        }

        return field.get(object);
    }

    /**
     * Converts the given identifier to a valid field name by stripping away
     * illegal character and setting the first letter of the name to lower case.
     *
     * @param identifier
     *            the identifier to be converted to field name
     * @return the field name corresponding the identifier
     */
    private static String asFieldName(String identifier) {
        if (identifier == null) {
            return "";
        }
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < identifier.length(); i++) {
            char character = identifier.charAt(i);
            if (Character.isJavaIdentifierPart(character)) {
                result.append(character);
            }
        }
        // lowercase first letter
        if (result.length() != 0 && Character.isLetter(result.charAt(0))) {
            result.setCharAt(0, Character.toLowerCase(result.charAt(0)));
        }
        return result.toString();
    }

    /**
     * Returns a list containing Field objects reflecting all the fields of the
     * class or interface represented by this Class object. The fields in
     * superclasses are excluded.
     *
     * @param searchClass
     *            the class to be scanned for fields
     * @return the list of fields in this class
     */
    protected static List getFields(
            Class searchClass) {
        List memberFields = new ArrayList<>();

        memberFields.addAll(Arrays.asList(searchClass.getDeclaredFields()));
        return memberFields;
    }

    private static Logger getLogger() {
        return Logger.getLogger(FieldBinder.class.getName());
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy