9

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.

justAbit
  • 4,226
  • 2
  • 19
  • 34

2 Answers2

9

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.

Tunaki
  • 132,869
  • 46
  • 340
  • 423
  • 2
    @Tom Oh correct, I didn't notice and edited. Thanks! – Tunaki Apr 28 '16 at 11:20
  • 2
    @Tom: this only reflects the inconsistency of the question itself, having “Count null fields” in the title but “count non `null` fields” in its body. – Holger Apr 28 '16 at 11:51
  • 2
    @Holger Thanks for pointing that out, I have updated question title. – justAbit Apr 28 '16 at 12:01
  • 2
    Of course, instead of `.map(f -> f.apply(t)).filter(Objects::nonNull)` you could just write `.filter(f -> f.apply(t)!=null)`, but that’s a matter of taste. – Holger Apr 28 '16 at 12:04
0

I wrote a utility class 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, nested properties, (multi-dimensional) iterables and maps.

I couldn't include the tests as well in here, because of the character limit, but here's the utility class:

import lombok.*;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class PropertyCountUtils {

    /***
     * See {@link #getReadablePropertyValueCount(Object, Set)}.
     */
    public static PropertyValueCount getReadablePropertyValueCount(@NonNull Object object) {
        return getReadablePropertyValueCount(object, null);
    }

    /**
     * Counts the properties of the given object, including inherited and nested properties,
     * returning the total property count and the count of properties with assigned values.
     *
     * <p>
     * Properties with assigned values have a value meeting all conditions below:
     * <ul>
     * <li>different from null</li>
     * <li>different from an empty iterable or an empty map</li>
     * <li>different from an iterable containing only null values</li>
     * <li>different from a map containing only null values.</li>
     * </ul>
     * For multidimensional Iterables and Maps, these conditions apply to each dimension.
     * </p>
     *
     * @param object            The object to inspect. It should not be null.
     * @param ignoredProperties The properties to ignore or null.
     *                          For nested properties, use dot as a separator: "property1.nestedProperty.nestedProperty2"
     * @return A pair of `assignedValueCount` (properties with assigned value) and `totalCount` (total property count).
     */
    public static PropertyValueCount getReadablePropertyValueCount(
            @NonNull Object object, Set<String> ignoredProperties) {
        PropertyValueCount countHolder = new PropertyValueCount();
        processReadablePropertyValueCount(countHolder, object, ignoredProperties, null);
        return countHolder;
    }

    /***
     * @return true if the object had at least one non-null property value or no readable properties.
     * <p>
     * If the object is an instance of String, for example, it would have no readable nested properties.
     * Also, if the object is an instance of some class for which all nested properties are ignored,
     * the method would return true, since the object itself has a non-null value,
     * but the caller decided to ignore all properties.
     * </p>
     */
    @SneakyThrows
    private static boolean processReadablePropertyValueCount(
            PropertyValueCount countHolder, @NonNull Object object, Set<String> ignoredProperties, String parentPath) {
        boolean objectHasAssignedProperties = false;
        boolean objectHasNoReadableProperties = true;

        List<Field> fields = getAllDeclaredFields(object.getClass());

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

            Method readMethod = getReadMethod(object.getClass(), ignoredProperties, field, fieldPath);
            if (readMethod == null) {
                continue;
            }

            countHolder.setTotalCount(countHolder.getTotalCount() + 1);
            objectHasNoReadableProperties = false;

            Object value = readMethod.invoke(object);

            if (value == null || isCollectionWithoutAnyNonNullValue(value)) {

                // no assigned value, so we'll just count the total available properties
                int readablePropertyValueCount = getReadablePropertyCount(
                        readMethod.getGenericReturnType(), ignoredProperties, fieldPath);
                countHolder.setTotalCount(countHolder.getTotalCount() + readablePropertyValueCount);

            } else if (value instanceof Iterable<?> iterable) {
                processPropertyValueCountInIterable(countHolder, ignoredProperties, fieldPath, iterable);

            } else if (value instanceof Map<?, ?> map) {
                processPropertyValueCountInIterable(countHolder, ignoredProperties, fieldPath, map.values());

            } else {
                countHolder.setAssignedValueCount(countHolder.getAssignedValueCount() + 1);

                // process properties of nested object
                processReadablePropertyValueCount(countHolder, value, ignoredProperties, fieldPath);
                objectHasAssignedProperties = true;
            }
        }

        return objectHasAssignedProperties || objectHasNoReadableProperties;
    }

    private static void processPropertyValueCountInIterable(
            PropertyValueCount countHolder, Set<String> ignoredProperties, String fieldPath, Iterable<?> iterable) {
        boolean iterableHasNonNullValues = false;

        // process properties of each item in the iterable
        for (Object value : iterable) {
            if (value != null) {
                // check if the current iterable item is also an iterable itself
                Optional<Iterable<?>> nestedIterable = getProcessableCollection(value);
                if (nestedIterable.isPresent()) {
                    processPropertyValueCountInIterable(countHolder, ignoredProperties, fieldPath, nestedIterable.get());
                } else {
                    iterableHasNonNullValues = processReadablePropertyValueCount(
                            countHolder, value, ignoredProperties, fieldPath);
                }
            }
        }

        // consider the iterable as having an assigned value only if it contains at least one non-null value
        if (iterableHasNonNullValues) {
            countHolder.setAssignedValueCount(countHolder.getAssignedValueCount() + 1);
        }
    }

    @SneakyThrows
    private static int getReadablePropertyCount(
            @NonNull Type inspectedType, Set<String> ignoredProperties, String parentPath) {
        int totalReadablePropertyCount = 0;

        Class<?> inspectedClass = getTargetClassFromGenericType(inspectedType);
        List<Field> fields = getAllDeclaredFields(inspectedClass);

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

            Method readMethod = getReadMethod(inspectedClass, ignoredProperties, field, fieldPath);

            if (readMethod != null) {
                totalReadablePropertyCount++;

                Class<?> returnType = getTargetClassFromGenericType(readMethod.getGenericReturnType());

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

                    totalReadablePropertyCount += readablePropertyValueCount;
                }
            }
        }

        return totalReadablePropertyCount;
    }

    // In case the object being analyzed is of parameterized type,
    // we want to count the properties in the class of the parameter, not of the container.
    private static Class<?> getTargetClassFromGenericType(Type type) {
        if (type instanceof ParameterizedType parameterizedType) {
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();

            if (actualTypeArguments.length > 0) {
                // Inspect the last parameter type.
                // For example, lists would only have one parameter type,
                // but in the case of maps we would inspect the parameter representing the entry value, not the entry key.
                Type inspectedTypeArgument = actualTypeArguments[actualTypeArguments.length - 1];

                return inspectedTypeArgument instanceof ParameterizedType ?
                        getTargetClassFromGenericType(inspectedTypeArgument) :
                        (Class<?>) inspectedTypeArgument;
            }
        }

        return type instanceof Class<?> ? (Class<?>) type : type.getClass();
    }

    private static List<Field> getAllDeclaredFields(@NonNull Class<?> inspectedClass) {
        List<Field> fields = new ArrayList<>();
        Collections.addAll(fields, inspectedClass.getDeclaredFields());

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

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

    private static Method getReadMethod(@NonNull Class<?> inspectedClass, Set<String> ignoredProperties, Field field, String fieldPath) {
        if (ignoredProperties != null && ignoredProperties.contains(fieldPath)) {
            return null;
        }

        PropertyDescriptor propertyDescriptor;
        try {
            propertyDescriptor = new PropertyDescriptor(field.getName(), inspectedClass);
        } catch (IntrospectionException e) {
            // statement reached when the field doesn't have a getter
            return null;
        }

        return propertyDescriptor.getReadMethod();
    }

    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();
    }

    private static boolean isCollectionWithoutAnyNonNullValue(Object value) {
        Stream<?> stream = null;
        if (value instanceof Iterable<?> iterable) {
            stream = StreamSupport.stream(iterable.spliterator(), false);
        } else if (value instanceof Map<?, ?> map) {
            stream = map.values().stream();
        }

        return stream != null &&
                stream.noneMatch(item -> item != null && !isCollectionWithoutAnyNonNullValue(item));
    }

    private static Optional<Iterable<?>> getProcessableCollection(Object value) {
        if (value instanceof Iterable<?> iterable) {
            return Optional.of(iterable);
        } else if (value instanceof Map<?, ?> map) {
            return Optional.of(map.values());
        }
        return Optional.empty();
    }

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

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();
flaviuratiu
  • 158
  • 8