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

com.addthis.codec.reflection.CodableFieldInfo Maven / Gradle / Ivy

There is a newer version: 3.8.2
Show newest version
/*
 * 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 com.addthis.codec.reflection;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Type;

import java.util.Collection;
import java.util.Map;

import com.addthis.codec.annotations.FieldConfig;
import com.addthis.codec.codables.Codable;

import com.google.common.annotations.Beta;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * information about a field in a class - expensive to get so runs and gets cached
 */
@Beta
public final class CodableFieldInfo {

    private static final Logger log = LoggerFactory.getLogger(CodableFieldInfo.class);

    public static final int ARRAY      = 1 << 0;
    public static final int CODABLE    = 1 << 1;
    public static final int COLLECTION = 1 << 2;
    public static final int GENERIC    = 1 << 3;
    public static final int NATIVE     = 1 << 4;
    public static final int MAP        = 1 << 5;
    public static final int NUMBER     = 1 << 6;
    public static final int REQUIRED   = 1 << 7;
    public static final int READONLY   = 1 << 8;
    public static final int WRITEONLY  = 1 << 9;
    public static final int ENUM       = 1 << 10;

    @Nonnull private final Field    field;
    @Nonnull private final Class typeOrComponentType;
    private final int bits;

    @Nullable private final FieldConfig fieldConfig;
    @Nullable private final Type[]      genTypes;
    @Nullable private final boolean[]   genArray;

    public CodableFieldInfo(@Nonnull Field field) {
        this.field = field;
        field.setAccessible(true);
        fieldConfig = field.getAnnotation(FieldConfig.class);

        Class type = field.getType();
        boolean array = type.isArray();
        if (array) {
            typeOrComponentType = type.getComponentType();
            this.bits = cacheFlags(CodableFieldInfo.ARRAY);
        } else {
            typeOrComponentType = type;
            this.bits = cacheFlags(0);
        }
        // extract generics info
        if (!Fields.isNative(typeOrComponentType)) {
            genTypes = Fields.collectTypes(typeOrComponentType, field.getGenericType());
        } else {
            genTypes = null;
        }
        if (genTypes == null) {
            genArray = null;
        } else {
            genArray = new boolean[genTypes.length];
            mutateGenericTypes(genTypes, genArray);
        }
    }

    private int cacheFlags(int externalBits) {
        int partialBits = externalBits;
        if (Codable.class.isAssignableFrom(typeOrComponentType)) {
            partialBits |= CodableFieldInfo.CODABLE;
        }
        if (Collection.class.isAssignableFrom(typeOrComponentType)) {
            partialBits |= CodableFieldInfo.COLLECTION;
        }
        if (Map.class.isAssignableFrom(typeOrComponentType)) {
            partialBits |= CodableFieldInfo.MAP;
        }
        if (typeOrComponentType.isEnum()) {
            partialBits |= CodableFieldInfo.ENUM;
        }
        if (Number.class.isAssignableFrom(typeOrComponentType)) {
            partialBits |= CodableFieldInfo.NUMBER;
        }
        if (Fields.isNative(typeOrComponentType)) {
            partialBits |= CodableFieldInfo.NATIVE;
        }
        if (fieldConfig != null) {
            if (fieldConfig.readonly()) {
                partialBits |= CodableFieldInfo.READONLY;
            }
            if (fieldConfig.writeonly()) {
                partialBits |= CodableFieldInfo.WRITEONLY;
            }
            if (fieldConfig.codable()) {
                partialBits |= CodableFieldInfo.CODABLE;
            }
            if (fieldConfig.required()) {
                partialBits |= CodableFieldInfo.REQUIRED;
            }
        }
        return partialBits;
    }

    @Nonnull public Field getField() {
        return field;
    }

    public String getName() {
        return field.getName();
    }

    @Nonnull public Class getTypeOrComponentType() {
        return typeOrComponentType;
    }

    @Nullable public Type[] getGenericTypes() {
        return genTypes;
    }

    // interacts with the ill-defined generic support
    private void mutateGenericTypes(@Nonnull final Type[] collectedTypes,
                                    @Nonnull final boolean[] genericFlags) {
        for (int i = 0; i < collectedTypes.length; i++) {
            Type currentType = collectedTypes[i];
            if (currentType instanceof GenericArrayType) {
                genericFlags[i] = true;
                collectedTypes[i] = ((GenericArrayType) currentType).getGenericComponentType();
            } else if ((currentType instanceof Class) && ((Class) currentType).isArray()) {
                genericFlags[i] = true;
                collectedTypes[i] = ((Class) currentType).getComponentType();
            } else {
                genericFlags[i] = false;
            }
        }
    }

    public Object newInstance() throws Exception {
        return typeOrComponentType.newInstance();
    }

    @Nullable public Class getCollectionClass() {
        return ((genTypes != null) && (genTypes.length == 1)) ? (Class) genTypes[0] : null;
    }

    @Nullable public Class getMapKeyClass() {
        return ((genTypes != null) && (genTypes.length == 2)) ? (Class) genTypes[0] : null;
    }

    @Nullable public Class getMapValueClass() {
        return ((genTypes != null) && (genTypes.length == 2)) ? (Class) genTypes[1] : null;
    }

    public boolean isCollectionArray() {
        return ((genArray != null) && (genArray.length == 1)) ? genArray[0] : false;
    }

    public boolean isMapKeyArray() {
        return ((genArray != null) && (genArray.length == 2)) ? genArray[0] : false;
    }

    public boolean isMapValueArray() {
        return ((genArray != null) && (genArray.length == 2)) ? genArray[1] : false;
    }

    public Object get(Object src) {
        try {
            return field.get(src);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Sets the field value for the destination object if and only if it is non-null and passes the field's
     * validation method (if any). If the value is null and the field is marked as required, then an exception
     * is thrown, otherwise the null value is ignored and the current value (if any) is kept.
     */
    public void setStrict(@Nonnull Object dst, @Nullable Object value) throws IllegalAccessException {
        if (value == null) {
            if (isRequired()) {
                throw new RequiredFieldException("missing required field '" +
                                                 this.getName() + "' for " + dst, getName());
            }
            return;
        }
        field.set(dst, value);
    }

    public void set(@Nonnull Object dst, @Nullable Object value) {
        if (value == null) {
            if (isRequired() && (get(dst) == null)) {
                throw new RequiredFieldException("missing required field '" +
                    this.getName() + "' for " + dst, getName());
            }
            return;
        }
        try {
            field.set(dst, value);
        } catch (RuntimeException ex) {
            throw ex;
        } catch (Exception ex) {
            log.warn("error setting ({})({}) on ({}) in {}", value, value.getClass(), dst, toString());
            throw new RuntimeException(ex);
        }
    }

    public boolean isArray() {
        return (bits & ARRAY) == ARRAY;
    }

    public boolean isCodable() {
        return (bits & CODABLE) == CODABLE;
    }

    public boolean isCollection() {
        return (bits & COLLECTION) == COLLECTION;
    }

    public boolean isGeneric() {
        return (bits & GENERIC) == GENERIC;
    }

    public boolean isMap() {
        return (bits & MAP) == MAP;
    }

    public boolean isEnum() {
        return (bits & ENUM) == ENUM;
    }

    public boolean isNative() {
        return (bits & NATIVE) == NATIVE;
    }

    public boolean isRequired() {
        return (bits & REQUIRED) == REQUIRED;
    }

    public boolean isReadOnly() {
        return (bits & READONLY) == READONLY;
    }

    public boolean isWriteOnly() {
        return (bits & WRITEONLY) == WRITEONLY;
    }

    public String toString() {
        return "[" + getName() + "," + typeOrComponentType + (isArray() ? "[]," : ",") + Integer.toString(bits, 2) + "]";
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy