简体   繁体   中英

Count non null fields in an object

I have a UserProfile class which contains user's data as shown below:

class UserProfile {

  private String userId;
  private String displayName;
  private String loginId;
  private String role;
  private String orgId;
  private String email;
  private String contactNumber;
  private Integer age;
  private String address;

// few more fields ...

// getter and setter
}

I need to count non null fields to show how much percentage of the profile has been filled by the user. Also there are few fields which I do not want to consider in percentage calculation like: userId , loginId and displayName .

Simple way would be to use multiple If statements to get the non null field count but it would involve lot of boiler plate code and there is another class Organization for which I need to show completion percentage as well. So I created a utility function as show below:

public static <T, U> int getNotNullFieldCount(T t,
        List<Function<? super T, ? extends U>> functionList) {
    int count = 0;

    for (Function<? super T, ? extends U> function : functionList) {
        count += Optional.of(t).map(obj -> function.apply(t) != null ? 1 : 0).get();
    }

    return count;
}

And then I call this function as shown below:

List<Function<? super UserProfile, ? extends Object>> functionList = new ArrayList<>();
functionList.add(UserProfile::getAge);
functionList.add(UserProfile::getAddress);
functionList.add(UserProfile::getEmail);
functionList.add(UserProfile::getContactNumber);
System.out.println(getNotNullFieldCount(userProfile, functionList));

My question is, is this the best way I could count not null fields or I could improve it further. Please suggest.

You can simply a lot your code by creating a Stream over the given list of functions:

public static <T> long getNonNullFieldCount(T t, List<Function<? super T, ?>> functionList) {
    return functionList.stream().map(f -> f.apply(t)).filter(Objects::nonNull).count();
}

This will return the count of non- null fields returned by each function. Each function is mapped to the result of applying it to the given object and null fields are filtered out with the predicate Objects::nonNull .

I wrote some utility methods to get the total count of readable properties and the count of non null values in an object. The completion percentage can be calculated based on these.

It should work pretty well with inherited properties and with nested objects too. It will probably need some adjustments to inspect the content of collection properties as well.

Here's the utility class:

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.SneakyThrows;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

public class PropertyCountUtils {

    public static PropertyValueCount getReadablePropertyValueCount(Object object) {
        return getReadablePropertyValueCount(object, null);
    }

    public static PropertyValueCount getReadablePropertyValueCount(
            Object object, Set<String> ignoredProperties) {
        return getReadablePropertyValueCount(object, true, ignoredProperties, null);
    }

    @SneakyThrows
    private static PropertyValueCount getReadablePropertyValueCount(
            Object object, boolean recursively, Set<String> ignoredProperties, String parentPath) {
        if (object == null) {
            return null;
        }

        int totalReadablePropertyCount = 0;
        int nonNullValueCount = 0;

        List<Field> fields = getAllDeclaredFields(object);

        for (Field field : fields) {
            String fieldPath = buildFieldPath(parentPath, field);

            if (ignoredProperties != null && ignoredProperties.contains(fieldPath)) {
                continue;
            }

            PropertyDescriptor propertyDescriptor;
            try {
                propertyDescriptor = new PropertyDescriptor(field.getName(), object.getClass());
            } catch (IntrospectionException e) {
                // ignore field if it doesn't have a getter
                continue;
            }

            Method readMethod = propertyDescriptor.getReadMethod();
            if (readMethod == null) {
                // ignore field if not readable
                continue;
            }

            totalReadablePropertyCount++;

            Object value = readMethod.invoke(object);

            if (value == null) {
                int readablePropertyValueCount = getReadablePropertyCount(
                        readMethod.getReturnType(), ignoredProperties, fieldPath);
                totalReadablePropertyCount += readablePropertyValueCount;
            } else {
                nonNullValueCount++;

                // process properties of nested object
                if (recursively) {
                    boolean circularTypeReference = hasCircularTypeReference(object.getClass(), value.getClass());

                    PropertyValueCount propertyValueCount = getReadablePropertyValueCount(
                            value,
                            // avoid infinite loop
                            !circularTypeReference,
                            ignoredProperties, fieldPath);

                    totalReadablePropertyCount += propertyValueCount.getTotalCount();
                    nonNullValueCount += propertyValueCount.getNonNullValueCount();
                }
            }
        }

        return PropertyValueCount.builder()
                .totalCount(totalReadablePropertyCount)
                .nonNullValueCount(nonNullValueCount)
                .build();
    }

    @SneakyThrows
    private static int getReadablePropertyCount(
            Class<?> inspectedClass, Set<String> ignoredProperties, String parentPath) {
        int totalReadablePropertyCount = 0;

        Field[] fields = inspectedClass.getDeclaredFields();

        for (Field field : fields) {
            String fieldPath = buildFieldPath(parentPath, field);

            if (ignoredProperties != null && ignoredProperties.contains(fieldPath)) {
                continue;
            }

            PropertyDescriptor propertyDescriptor;
            try {
                propertyDescriptor = new PropertyDescriptor(field.getName(), inspectedClass);
            } catch (IntrospectionException e) {
                // ignore field if it doesn't have a getter
                continue;
            }

            Method readMethod = propertyDescriptor.getReadMethod();
            if (readMethod != null) {
                totalReadablePropertyCount++;

                Class<?> returnType = readMethod.getReturnType();

                // process properties of nested class, avoiding infinite loops
                if (!hasCircularTypeReference(inspectedClass, returnType)) {
                    int readablePropertyValueCount = getReadablePropertyCount(
                            returnType, ignoredProperties, fieldPath);

                    totalReadablePropertyCount += readablePropertyValueCount;
                }
            }
        }

        return totalReadablePropertyCount;
    }

    private static List<Field> getAllDeclaredFields(Object object) {
        List<Field> fields = new ArrayList<>();
        Collections.addAll(fields, object.getClass().getDeclaredFields());

        Class<?> superClass = object.getClass().getSuperclass();
        while (superClass != null) {
            Collections.addAll(fields, superClass.getDeclaredFields());

            superClass = superClass.getSuperclass();
        }
        return fields;
    }

    private static boolean hasCircularTypeReference(Class<?> propertyContainerClass, Class<?> propertyType) {
        return propertyContainerClass.isAssignableFrom(propertyType);
    }

    private static String buildFieldPath(String parentPath, Field field) {
        return parentPath == null ? field.getName() : parentPath + "." + field.getName();
    }

    @Data
    @AllArgsConstructor
    @Builder
    public static class PropertyValueCount {
        private int nonNullValueCount;
        private int totalCount;
    }
}

And here are some tests for it:

import lombok.*;
import lombok.experimental.SuperBuilder;
import org.junit.jupiter.api.Test;

import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;

public class PropertyCountUtilsTest {

    @Test
    public void getReadablePropertyValueCount_whenAllFieldsPopulated() {
        ChildClass object = ChildClass.builder()
                .propertyA("A")
                .propertyB(TestEnum.VALUE_1)
                .propertyC(1)
                .propertyD(NestedClass.builder()
                        .propertyX("X")
                        .propertyY(TestEnum.VALUE_2)
                        .propertyZ(2)
                        .nestedProperty(NestedClass.builder()
                                .propertyX("X'")
                                .propertyY(TestEnum.VALUE_3)
                                .propertyZ(3)
                                // `nestedProperty` and its properties should still be added to the total count
                                .build())
                        .build())
                .property1("test")
                .property2(TestEnum.VALUE_4)
                .property3(4)
                .property4(NestedClass.builder()
                        .propertyX("test X")
                        .propertyY(TestEnum.VALUE_5)
                        .propertyZ(2)
                        .nestedProperty(NestedClass.builder()
                                .propertyX("test X'")
                                .propertyY(TestEnum.VALUE_6)
                                .propertyZ(3)
                                // `nestedProperty` and its properties should still be added to the total count
                                .build())
                        .build())
                .build();

        PropertyCountUtils.PropertyValueCount propertyValueCount =
                PropertyCountUtils.getReadablePropertyValueCount(object);

        assertThat(propertyValueCount).isNotNull();
        assertThat(propertyValueCount.getTotalCount()).isEqualTo(32);
        assertThat(propertyValueCount.getNonNullValueCount()).isEqualTo(22);
    }

    @Test
    public void getReadablePropertyValueCount_whenSomeFieldsPopulated() {
        ChildClass object = ChildClass.builder()
                .propertyC(1)
                .property3(2)
                .build();

        PropertyCountUtils.PropertyValueCount propertyValueCount =
                PropertyCountUtils.getReadablePropertyValueCount(object);

        assertThat(propertyValueCount).isNotNull();
        // recursive nested properties only counted once
        assertThat(propertyValueCount.getTotalCount()).isEqualTo(16);
        assertThat(propertyValueCount.getNonNullValueCount()).isEqualTo(2);
    }

    @Test
    public void getReadablePropertyValueCount_whenSomeIgnoredProperties() {
        ChildClass object = ChildClass.builder()
                .propertyA("A")
                .propertyB(TestEnum.VALUE_1)
                .propertyC(1)
                .propertyD(NestedClass.builder()
                        .propertyX("X")
                        .propertyY(TestEnum.VALUE_2)
                        .propertyZ(2)
                        .nestedProperty(NestedClass.builder()
                                .propertyX("X'")
                                .propertyY(TestEnum.VALUE_3)
                                .propertyZ(3)
                                // `nestedProperty` and its properties should still be added to the total count
                                .build())
                        .build())
                .property1("test")
                .property2(TestEnum.VALUE_4)
                .property3(4)
                .property4(NestedClass.builder()
                        .propertyX("test X")
                        .propertyY(TestEnum.VALUE_5)
                        .propertyZ(2)
                        .nestedProperty(NestedClass.builder()
                                .propertyX("test X'")
                                .propertyY(TestEnum.VALUE_6)
                                .propertyZ(3)
                                // `nestedProperty` and its properties should still be added to the total count
                                .build())
                        .build())
                .build();

        PropertyCountUtils.PropertyValueCount propertyValueCount =
                PropertyCountUtils.getReadablePropertyValueCount(object,
                        Set.of("propertyA", "propertyD.propertyX", "propertyD.nestedProperty.propertyY"));

        assertThat(propertyValueCount).isNotNull();
        assertThat(propertyValueCount.getTotalCount()).isEqualTo(29);
        assertThat(propertyValueCount.getNonNullValueCount()).isEqualTo(19);
    }

    @Data
    @SuperBuilder
    private static class SuperClass {

        private String property1;
        private TestEnum property2;
        private int property3;
        private NestedClass property4;

    }

    @Data
    @EqualsAndHashCode(callSuper = true)
    @SuperBuilder
    private static class ChildClass extends SuperClass {

        private String propertyA;
        private TestEnum propertyB;
        private int propertyC;
        private NestedClass propertyD;

    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    private static class NestedClass {

        private String propertyX;
        private TestEnum propertyY;
        private int propertyZ;
        private NestedClass nestedProperty;

    }

    private enum TestEnum {

        VALUE_1,
        VALUE_2,
        VALUE_3,
        VALUE_4,
        VALUE_5,
        VALUE_6

    }

}

The completion percentage can be calculated like this:

        PropertyCountUtils.PropertyValueCount propertyValueCount = getReadablePropertyValueCount(profile);

        BigDecimal profileCompletionPercentage = BigDecimal.valueOf(propertyValueCount.getNonNullValueCount())
                .multiply(BigDecimal.valueOf(100))
                .divide(BigDecimal.valueOf(propertyValueCount.getTotalCount()), 2, RoundingMode.UP)
                .stripTrailingZeros();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM