Util class for dynamic attribute assigning

One of the pretty common case in Hybris customizations is to receive master data, for example Products, from external system and use hybris only for minor changes. In such cases business wants to have flexible system, which allows to customize attribute assigning via backoffice, impex files etc.

To make it possible hybris must store itemtypes which will define what attributes to set and use reflection to define setter method and invoke it. Below you can find util class, which provides low level API for dynamic invoking setters and getter by attribute names.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package com.blog.sap.core.utils;

import org.apache.http.util.Asserts;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import javax.annotation.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Locale;

public class ReflectionUtils {

    private static final Logger LOG = LoggerFactory.getLogger(ReflectionUtils.class);

    private ReflectionUtils() {
    }

    public static Object invokeGetterMethod(Object target, String name, Object... args) {
        Assert.notNull(target, "Target object must not be null");
        Assert.hasText(name, "Method name must not be empty");

        Method method = resolveMethod(target, name, "get", args);

        if (method == null) {
            throw new IllegalArgumentException(String.format("Could not find getter method '%s' on %s", name, safeToString(target)));
        } else {
            if (LOG.isDebugEnabled()) {
                LOG.debug(String.format("Invoking getter method '%s' on %s", name, safeToString(target)));
            }

            org.springframework.util.ReflectionUtils.makeAccessible(method);
            return org.springframework.util.ReflectionUtils.invokeMethod(method, target, args);
        }
    }

    public static void invokeSetterMethod(Object target, String name, Object... args) throws InvocationTargetException, IllegalAccessException {
        Assert.notNull(target, "Target object must not be null");
        Assert.hasText(name, "Method name must not be empty");

        Method method = resolveMethod(target, name, "set", args);

        if (method == null) {
            throw new IllegalArgumentException(String.format("Could not find setter method '%s' on %s", name, safeToString(target)));
        } else {
            method.invoke(target, args);
        }
    }

    public static boolean hasLocalizedSetter(Object target, String methodName, Object... args) {
        Asserts.notNull(target, "Product can't be null");

        // Add locale object to list of args
        Object[] argsWithLocale = Arrays.copyOf(args, args.length + 1);
        argsWithLocale[args.length] = Locale.class;

        return hasSetter(target, methodName, argsWithLocale);
    }

    public static boolean hasSetter(Object target, String methodName, Object... args) {
        Method method = ReflectionUtils.resolveMethod(target, methodName, "set", args);
        return method != null;
    }


    @Nullable
    private static Method resolveMethod(Object target, String name, String prefix, Object... args) {
        String prefixedMethodName = name;
        if (!name.startsWith(prefix)) {
            prefixedMethodName = prefix + StringUtils.capitalize(name);
        }

        Method resolvedMethod;
        Method[] methods = org.springframework.util.ReflectionUtils.getAllDeclaredMethods(target.getClass());

        resolvedMethod = findMethodByNameAndArgs(prefixedMethodName, methods, args);


        if (resolvedMethod == null && !prefixedMethodName.equals(name)) {
            resolvedMethod = findMethodByNameAndArgs(name, methods, args);
        }

        return resolvedMethod;
    }

    private static Method findMethodByNameAndArgs(String getterMethodName, Method[] methods, Object[] args) {
        for (Method method : methods) {
            if (method.getName().equalsIgnoreCase(getterMethodName) && method.getParameterCount() == args.length) {
                Class[] types = method.getParameterTypes();
                if (isSameParameters(args, types)) {
                    return method;
                }

            }
        }
        return null;
    }

    private static boolean isSameParameters(Object[] args, Class[] types) {
        if (args.length == 0) {
            return true;
        }
        for (int i = 0; i < types.length; i++) {
            if (args[i] instanceof Class) {
                if (!types[i].isAssignableFrom((Class) args[i])) {
                    return false;
                }
            } else {
                if (!types[i].isAssignableFrom(args[i].getClass())) {
                    return false;
                }
            }
        }
        return true;
    }

    private static String safeToString(Object target) {
        try {
            return String.format("target object [%s]", target);
        } catch (Exception var2) {
            return String.format("target of type [%s] whose toString() method threw [%s]", target != null ? target.getClass().getName() : "unknown", var2);
        }
    }

}

This util class is pretty flexible and can be used in any situation, but sometimes performance is more important than flexibility. In such case can be used java MethodHandle approach. But MethodHandle approach is not always able to find and execute methods. It has limitation on setters for List and Set attributes. Usually hybris generates in such cases setters with Collection class parameter, which is used in MethodHandle approach. In case of Hybris generates List or Set classes in method signature MethodHandleUtils must be modified or ReflectionUtils must be used. ReflectionUtils uses isAssignableFrom check which allows to bypass such limitation of MethodHandle approach.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180

package com.blog.sap.core.utils;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.util.Asserts;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Arrays;
import java.util.Collection;
import java.util.Locale;

public class MethodHandleUtils {

    private static final Logger LOG = LoggerFactory.getLogger(MethodHandleUtils.class);

    private MethodHandleUtils() {
    }

    public static boolean hasSetter(Object targetObject, String methodName, Object... methodArguments) {
        MethodHandle setterMethod = resolveHybrisSetterMethodHandler(targetObject, methodName, methodArguments);
        return setterMethod != null;
    }

    public static boolean hasLocalizedSetter(Object targetObject, String methodName, Object... methodArguments) {
        Asserts.notNull(targetObject, "Product can't be null");

        // Add locale object to list of args
        Object[] argsWithLocale = Arrays.copyOf(methodArguments, methodArguments.length + 1);
        argsWithLocale[methodArguments.length] = Locale.class;

        return hasSetter(targetObject, methodName, argsWithLocale);
    }

    public static <T> T invokeGetterMethod(Object targetObject, String methodName, Class resultClass, Object... methodArguments) {
        Assert.notNull(targetObject, "Target object must not be null");
        Assert.hasText(methodName, "Method name must not be empty");


        MethodHandle getterMethod = resolveMethodHandle(targetObject, "get", methodName, resultClass, methodArguments);

        if (getterMethod == null) {
            throw new IllegalArgumentException(String.format("Could not find getter method '%s' on %s", methodName, safeToString(targetObject)));
        } else {
            if (LOG.isDebugEnabled()) {
                LOG.debug(String.format("Invoking getter method '%s' on %s", methodName, safeToString(targetObject)));
            }

            Object[] dataArrayForInvocation = createDataArrayForInvocation(targetObject, methodArguments);
            try {
                return (T) getterMethod.invokeWithArguments(dataArrayForInvocation);
            } catch (Throwable e) {
                throw new InvocationException(e);
            }
        }
    }

    public static void invokeSetterMethod(Object targetObject, String methodName, Object... methodArguments) {
        Assert.notNull(targetObject, "Target object must not be null");
        Assert.hasText(methodName, "Method name must not be empty");

        MethodHandle setterMethod = resolveHybrisSetterMethodHandler(targetObject, methodName, methodArguments);

        if (setterMethod != null) {
            Object[] dataArrayForInvocation = createDataArrayForInvocation(targetObject, methodArguments);
            try {
                setterMethod.invokeWithArguments(dataArrayForInvocation);
            } catch (Throwable e) {
                throw new InvocationException(e);
            }
        } else {
            throw new IllegalArgumentException(String.format("Could not find setter method '%s' on %s", methodName, safeToString(targetObject)));
        }

    }

    private static MethodHandle resolveHybrisSetterMethodHandler(Object targetObject, String methodName, Object[] methodArguments) {
        // Check if has collection attribute in arguments
        boolean hasCollectionAttribute = false;
        for (Object methodArgument : methodArguments) {
            if (methodArgument instanceof Collection) {
                hasCollectionAttribute = true;
                break;
            }
        }

        // Try to find method handler with collection attribute class
        MethodHandle setterMethod = null;
        if (hasCollectionAttribute) {
            setterMethod = resolveSetterMethodHandleWithCollectionAttribute(targetObject, methodName, methodArguments);
        }

        // Try to find method handler as is
        if (setterMethod == null) {
            setterMethod = resolveMethodHandle(targetObject, "set", methodName, void.class, methodArguments);
        }
        return setterMethod;
    }


    private static MethodHandle resolveSetterMethodHandleWithCollectionAttribute(Object targetObject, String methodName, Object[] methodArguments) {

        // Usually hybris creates setter with Collection.class argument
        Class[] argumentClasses = new Class[methodArguments.length];
        for (int i = 0; i < methodArguments.length; i++) {
            if (methodArguments[i] instanceof Class) {
                argumentClasses[i] = (Class) methodArguments[i];
            } else if (methodArguments[i] instanceof Collection) {
                argumentClasses[i] = Collection.class;
            } else {
                argumentClasses[i] = methodArguments.getClass();
            }
        }

        return resolveMethodHandle(targetObject, "set", methodName, void.class, argumentClasses);
    }

    private static MethodHandle resolveMethodHandle(Object targetObject, String methodNamePrefix, String methodName, Class resultClass, Object[] methodArguments) {
        final String prefixedMethodName;
        if (StringUtils.isNotBlank(methodNamePrefix) && !methodName.startsWith(methodNamePrefix)) {
            prefixedMethodName = methodNamePrefix + StringUtils.capitalize(methodName);
        } else {
            prefixedMethodName = methodName;
        }

        Class[] argumentClasses = new Class[methodArguments.length];
        for (int i = 0; i < methodArguments.length; i++) {
            if (methodArguments[i] instanceof Class) {
                argumentClasses[i] = (Class) methodArguments[i];
            } else {
                argumentClasses[i] = methodArguments[i].getClass();
            }
        }

        MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
        final MethodType methodType = MethodType.methodType(resultClass, argumentClasses);


        MethodHandle methodHandle = safeExecute(() -> publicLookup.findVirtual(targetObject.getClass(), prefixedMethodName, methodType));
        if (methodHandle == null && !prefixedMethodName.equals(methodName)) {
            methodHandle = safeExecute(() -> publicLookup.findVirtual(targetObject.getClass(), methodName, methodType));
        }

        return methodHandle;
    }

    private static Object[] createDataArrayForInvocation(Object targetObject, Object... arguments) {
        Object[] resultArray = new Object[arguments.length + 1];
        resultArray[0] = targetObject;
        System.arraycopy(arguments, 0, resultArray, 1, arguments.length);
        return resultArray;

    }

    private static String safeToString(Object target) {
        try {
            return String.format("target object [%s]", target);
        } catch (Exception var2) {
            return String.format("target of type [%s] whose toString() method threw [%s]", target != null ? target.getClass().getName() : "unknown", var2);
        }
    }

    @FunctionalInterface
    public interface SupplierWithReflectiveOperationException<T> {
        T get() throws ReflectiveOperationException;
    }

    private static <T> T safeExecute(SupplierWithReflectiveOperationException<T> supplier) {
        try {
            return supplier.get();
        } catch (ReflectiveOperationException e) {
            return null;
        }
    }

}

Sample example of API usage:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package com.blog.sap.core.service.impl;

import com.blog.sap.core.model.AbstractSapStringAttributeParserModel;
import com.blog.sap.core.model.ParsedValueMapperModel;
import com.blog.sap.core.model.ProductAttributeParserModel;
import com.blog.sap.core.sap.attribute.ParsedValueMapperDAO;
import com.blog.sap.core.service.ParsedValueMapperService;
import com.blog.sap.core.utils.MethodHandleUtils;
import de.hybris.platform.core.model.ItemModel;
import de.hybris.platform.core.model.product.ProductModel;
import de.hybris.platform.servicelayer.i18n.I18NService;
import de.hybris.platform.servicelayer.model.ModelService;
import de.hybris.platform.servicelayer.search.FlexibleSearchService;
import org.apache.commons.collections.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;

public class ProductAttributeMapperService implements ParsedValueMapperService {

    private static final Locale DEFAULT_LOCALE = Locale.ENGLISH;

    private static final Logger LOG = LoggerFactory.getLogger(ProductAttributeMapperService.class);
    private static final String CODE_ATTRIBUTE = "code";
    private static final String CATALOG_VERSION_ATTRIBUTE = "catalogVersion";

    @Resource
    private I18NService i18NService;

    @Resource
    private ParsedValueMapperDAO parsedValueMapperDAO;

    @Resource
    private ModelService modelService;

    @Resource
    private FlexibleSearchService flexibleSearchService;

    @Override
    public void clearProductAttribute(ProductModel product, Object attribute) {
        if (attribute instanceof String) {
            try {
                if (MethodHandleUtils.hasSetter(product, (String) attribute, Collection.class)) {
                    MethodHandleUtils.invokeSetterMethod(product, (String) attribute, new ArrayList<>());
                }
            } catch (Exception exception) {
                LOG.warn("Product {} is not change. Attribute {} was not cleared.", product.getCode(), attribute);
                if (LOG.isDebugEnabled()) {
                    LOG.warn("Product {} is not change. Attribute {} was not cleared.", product.getCode(), attribute, exception);
                }
            }
        }
    }

    @Override
    public void mapParsedValue(ProductModel product, AbstractSapStringAttributeParserModel abstractParser, String attributeValue) {
        if (abstractParser instanceof ProductAttributeParserModel) {
            ProductAttributeParserModel parser = (ProductAttributeParserModel) abstractParser;
            try {
                setProductAttribute(product, parser, attributeValue);
            } catch (Exception exception) {
                LOG.warn("Product {} is not change. Attribute {} with value {} is not set.", product.getCode(), parser.getCode(), attributeValue);
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Product {} is not change. Attribute {} with value {} is not set.", product.getCode(), parser.getCode(), attributeValue, exception);
                }
            }
        }
    }


    private void setProductAttribute(ProductModel product, ProductAttributeParserModel parser, String attributeValue) throws ClassNotFoundException {

        String attribute = parser.getProductAttribute();
        Class attributeType = Class.forName(parser.getProductAttributeClassType());

        ParsedValueMapperModel mapper = parsedValueMapperDAO.getParsedValueMapperByCodeAndParsedValue(parser.getCode(), attributeValue);

        // Set string into product attribute
        if (attributeType == String.class) {
            setStringAttribute(product, mapper, attribute, attributeValue);
        } else if (Boolean.class == attributeType) {
            // Set boolean into product attribute
            setBooleanAttribute(product, mapper, attribute, attributeValue);
        } else if (ItemModel.class.isAssignableFrom(attributeType)) {
            // Set Item type into product attribute
            boolean isCollection = parser.getIsCollectionAttribute();
            if (!isCollection) {
                Class setterSignatureClass = Class.forName(parser.getProductAttributeClassType());
                if (MethodHandleUtils.hasSetter(product, attribute, setterSignatureClass)) {
                    ItemModel attributeValueModel = getAttributeModel(product.getCatalogVersion(), attributeType, mapper, attributeValue);
                    setItemModelAttribute(product, attribute, attributeValueModel);
                }
            } else {
                // Set collection of item types into product attribute
                if (MethodHandleUtils.hasSetter(product, attribute, Collection.class)) {
                    ItemModel attributeValueModel = getAttributeModel(product.getCatalogVersion(), attributeType, mapper, attributeValue);
                    setCollectionItemModelAttribute(product, attribute, attributeValueModel);
                }
            }
        }

    }

    private void setBooleanAttribute(ProductModel product, ParsedValueMapperModel mapper, String attribute, String attributeValue) {
        if (MethodHandleUtils.hasSetter(product, attribute, Boolean.class)) {
            if (mapper == null) {
                MethodHandleUtils.invokeSetterMethod(product, attribute, Boolean.valueOf(attributeValue));
            } else {
                MethodHandleUtils.invokeSetterMethod(product, attribute, Boolean.valueOf(mapper.getMappedValue(DEFAULT_LOCALE)));
            }
        }
    }

    private void setStringAttribute(ProductModel product, ParsedValueMapperModel mapper, String attribute, String attributeValue) {
        if (MethodHandleUtils.hasSetter(product, attribute, String.class)) {
            if (mapper == null) {
                MethodHandleUtils.invokeSetterMethod(product, attribute, attributeValue);
            } else {
                if (MethodHandleUtils.hasLocalizedSetter(product, attribute, String.class)) {
                    for (Locale locale : i18NService.getSupportedLocales()) {
                        MethodHandleUtils.invokeSetterMethod(product, attribute, mapper.getMappedValue(locale), locale);
                    }
                } else {
                    MethodHandleUtils.invokeSetterMethod(product, attribute, mapper.getMappedValue(DEFAULT_LOCALE));
                }
            }
        }
    }

    private void setItemModelAttribute(ProductModel product, String attribute, ItemModel attributeValueModel) {
        if (attributeValueModel != null) {
            MethodHandleUtils.invokeSetterMethod(product, attribute, attributeValueModel);
        }
    }


    private void setCollectionItemModelAttribute(ProductModel product, String attribute, ItemModel attributeValueModel) {
        if (attributeValueModel != null) {
            Collection<ItemModel> oldValues = MethodHandleUtils.invokeGetterMethod(product, attribute, Collection.class);
            Collection<ItemModel> newValues;
            if (CollectionUtils.isEmpty(oldValues)) {
                newValues = new HashSet<>();
            } else {
                newValues = new HashSet<>(oldValues);
            }
            newValues.add(attributeValueModel);
            MethodHandleUtils.invokeSetterMethod(product, attribute, newValues);
        }
    }

    private ItemModel getAttributeModel(CatalogVersionModel productCatalogVersion, Class modelType, ParsedValueMapperModel mapper, String defaultModelCode) {
        ItemModel exampleAttributeValueModel = modelService.create(modelType);

        // Set catalog version if attribute supports it
        if (MethodHandleUtils.hasSetter(exampleAttributeValueModel, CATALOG_VERSION_ATTRIBUTE, CatalogVersionModel.class) && productCatalogVersion != null) {
            MethodHandleUtils.invokeSetterMethod(exampleAttributeValueModel, CATALOG_VERSION_ATTRIBUTE, productCatalogVersion);
        }

        // Set item code and execute sear
        if (MethodHandleUtils.hasSetter(exampleAttributeValueModel, CODE_ATTRIBUTE, String.class)) {
            String mappedAttributeValue;
            if (mapper != null) {
                mappedAttributeValue = mapper.getMappedValue(DEFAULT_LOCALE);
            } else {
                mappedAttributeValue = defaultModelCode;
            }

            MethodHandleUtils.invokeSetterMethod(exampleAttributeValueModel, CODE_ATTRIBUTE, mappedAttributeValue);
            List<ItemModel> attributeValueModels = flexibleSearchService.getModelsByExample(exampleAttributeValueModel);

            if (CollectionUtils.isNotEmpty(attributeValueModels)) {
                return attributeValueModels.get(0);
            }
        }
        return null;
    }
}
comments powered by Disqus