package com.thinkbiganalytics.policy;
/*-
* #%L
* thinkbig-field-policy-common
* %%
* Copyright (C) 2017 ThinkBig Analytics
* %%
* 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.
* #L%
*/
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.thinkbiganalytics.annotations.AnnotatedFieldProperty;
import com.thinkbiganalytics.annotations.AnnotationFieldNameResolver;
import com.thinkbiganalytics.policy.rest.model.BaseUiPolicyRule;
import com.thinkbiganalytics.policy.rest.model.FieldRuleProperty;
import com.thinkbiganalytics.policy.rest.model.FieldRulePropertyBuilder;
import com.thinkbiganalytics.rest.model.LabelValue;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Transform classes annotated with a class annotation and fields with {@link PolicyProperty} to user interface {@link BaseUiPolicyRule} objects for input display in the browser and back.
*/
public abstract class BasePolicyAnnotationTransformer<U extends BaseUiPolicyRule, P extends Object, A extends Annotation>
implements PolicyTransformer<U, P, A> {
private static final Logger log = LoggerFactory.getLogger(BasePolicyAnnotationTransformer.class);
/**
* For a given domain policy class extract the user interface {@link FieldRuleProperty} classes parsing the fields annotated with the {@link PolicyProperty}
*
* @param policy the domain policy object to parse
* @return a list of user interface fields annotated with the {@link PolicyProperty}
*/
private List<FieldRuleProperty> getUiProperties(P policy) {
AnnotationFieldNameResolver annotationFieldNameResolver = new AnnotationFieldNameResolver(PolicyProperty.class);
List<AnnotatedFieldProperty> list = annotationFieldNameResolver.getProperties(policy.getClass());
List<FieldRuleProperty> properties = new ArrayList<>();
Map<String, Integer> groupOrder = new HashMap<>();
Map<String, List<FieldRuleProperty>> groupedProperties = new HashMap<>();
if (hasConstructor(policy.getClass())) {
for (AnnotatedFieldProperty<PolicyProperty> annotatedFieldProperty : list) {
PolicyProperty prop = annotatedFieldProperty.getAnnotation();
String value = null;
try {
Object fieldValue = FieldUtils.readField(annotatedFieldProperty.getField(), policy, true);
if (fieldValue != null) {
value = fieldValue.toString();
}
} catch (IllegalAccessException e) {
}
String group = prop.group();
Integer order = 0;
if (!groupOrder.containsKey(group)) {
groupOrder.put(group, order);
}
order = groupOrder.get(group);
order++;
groupOrder.put(group, order);
FieldRuleProperty rule = new FieldRulePropertyBuilder(prop.name()).displayName(
StringUtils.isNotBlank(prop.displayName()) ? prop.displayName() : prop.name()).hint(prop.hint())
.type(PolicyPropertyTypes.PROPERTY_TYPE.valueOf(prop.type().name()))
.objectProperty(annotatedFieldProperty.getName())
.placeholder(prop.placeholder())
.value(value)
.required(prop.required())
.group(group)
.groupOrder(order)
.pattern(prop.pattern())
.patternInvalidMessage(prop.patternInvalidMessage())
.hidden(prop.hidden())
.addSelectableValues(convertToLabelValue(prop.selectableValues()))
.addSelectableValues(convertToLabelValue(prop.labelValues())).build();
properties.add(rule);
if (!group.equals("")) {
if (!groupedProperties.containsKey(group)) {
groupedProperties.put(group, new ArrayList<FieldRuleProperty>());
}
groupedProperties.get(group).add(rule);
}
}
//update layout property
for (Collection<FieldRuleProperty> groupProps : groupedProperties.values()) {
for (FieldRuleProperty property : groupProps) {
property.setLayout("row");
}
}
}
return properties;
}
/**
* For a given domain policy class extract the user interface {@link FieldRuleProperty} classes parsing the fields annotated with the {@link PolicyProperty}
*
* @param policyClass the domain policy class to parse
* @return a list of user interface fields annotated with the {@link PolicyProperty}
*/
public List<FieldRuleProperty> getUiProperties(Class<P> policyClass) {
AnnotationFieldNameResolver annotationFieldNameResolver = new AnnotationFieldNameResolver(PolicyProperty.class);
List<AnnotatedFieldProperty> list = annotationFieldNameResolver.getProperties(policyClass);
List<FieldRuleProperty> properties = new ArrayList<>();
Map<String, List<FieldRuleProperty>> groupedProperties = new HashMap<>();
if (hasConstructor(policyClass)) {
Map<String, Integer> groupOrder = new HashMap<>();
for (AnnotatedFieldProperty<PolicyProperty> annotatedFieldProperty : list) {
PolicyProperty prop = annotatedFieldProperty.getAnnotation();
String value = StringUtils.isBlank(prop.value()) ? null : prop.value();
String group = prop.group();
Integer order = 0;
if (!groupOrder.containsKey(group)) {
groupOrder.put(group, order);
}
order = groupOrder.get(group);
order++;
groupOrder.put(group, order);
FieldRuleProperty rule = new FieldRulePropertyBuilder(prop.name()).displayName(
StringUtils.isNotBlank(prop.displayName()) ? prop.displayName() : prop.name()).hint(prop.hint())
.type(PolicyPropertyTypes.PROPERTY_TYPE.valueOf(prop.type().name()))
.objectProperty(annotatedFieldProperty.getName())
.placeholder(prop.placeholder())
.value(value)
.required(prop.required())
.group(group)
.groupOrder(order)
.hidden(prop.hidden())
.pattern(prop.pattern())
.patternInvalidMessage(prop.patternInvalidMessage())
.addSelectableValues(convertToLabelValue(prop.selectableValues()))
.addSelectableValues(convertToLabelValue(prop.labelValues())).build();
properties.add(rule);
if (!group.equals("")) {
if (!groupedProperties.containsKey(group)) {
groupedProperties.put(group, new ArrayList<FieldRuleProperty>());
}
groupedProperties.get(group).add(rule);
}
}
//update layout property
for (Collection<FieldRuleProperty> groupProps : groupedProperties.values()) {
for (FieldRuleProperty property : groupProps) {
property.setLayout("row");
}
}
}
return properties;
}
/**
* Find all the user interface properies for a given render type
*/
public List<FieldRuleProperty> findPropertiesMatchingRenderType(List<FieldRuleProperty> properties, final String type) {
if (StringUtils.isNotBlank(type)) {
return findPropertiesMatchingRenderTypes(properties, new String[]{type});
}
return null;
}
public List<FieldRuleProperty> findPropertiesMatchingDefaultValue(List<FieldRuleProperty> properties, final String value) {
if (StringUtils.isNotBlank(value)) {
return findPropertiesMatchingRenderTypes(properties, new String[]{value});
}
return null;
}
public List<FieldRuleProperty> findPropertiesMatchingDefaultValue(List<FieldRuleProperty> properties, final String[] values) {
final List list = Arrays.asList(values);
return Lists.newArrayList(Iterables.filter(properties, new Predicate<FieldRuleProperty>() {
@Override
public boolean apply(FieldRuleProperty fieldRuleProperty) {
return list.contains(fieldRuleProperty.getValue());
}
}));
}
public List<FieldRuleProperty> findPropertiesMatchingRenderTypes(List<FieldRuleProperty> properties, final String[] types) {
final List list = Arrays.asList(types);
return Lists.newArrayList(Iterables.filter(properties, new Predicate<FieldRuleProperty>() {
@Override
public boolean apply(FieldRuleProperty fieldRuleProperty) {
return list.contains(fieldRuleProperty.getType());
}
}));
}
public List<FieldRuleProperty> findPropertiesForRulesetMatchingRenderType(List<? extends BaseUiPolicyRule> rules, final String type) {
List<FieldRuleProperty> properties = new ArrayList<>();
for (BaseUiPolicyRule rule : rules) {
properties.addAll(rule.getProperties());
}
return findPropertiesMatchingRenderType(properties, type);
}
public List<FieldRuleProperty> findPropertiesForRulesMatchingDefaultValue(List<? extends BaseUiPolicyRule> rules, final String value) {
List<FieldRuleProperty> properties = new ArrayList<>();
for (BaseUiPolicyRule rule : rules) {
properties.addAll(rule.getProperties());
}
return findPropertiesMatchingDefaultValue(properties, value);
}
public List<FieldRuleProperty> findPropertiesForRulesetMatchingRenderTypes(List<? extends BaseUiPolicyRule> rules, final String[] types) {
List<FieldRuleProperty> properties = new ArrayList<>();
for (BaseUiPolicyRule rule : rules) {
properties.addAll(rule.getProperties());
}
return findPropertiesMatchingRenderTypes(properties, types);
}
public abstract U buildUiModel(A annotation, P policy, List<FieldRuleProperty> properties);
public abstract Class<A> getAnnotationClass();
/**
* override to do anything special to the resulting obj from the UI
*/
public void afterFromUiModel(P policy, U uiModel) {
}
/**
* Transform a domain policy rule to a user interface object
*
* @param policyRule the domain object
* @return the user interface object
*/
@Override
public U toUIModel(P policyRule) {
Annotation annotation = policyRule.getClass().getAnnotation(getAnnotationClass());
List<FieldRuleProperty> properties = getUiProperties(policyRule);
U rule = buildUiModel((A) annotation, policyRule, properties);
return rule;
}
/**
* Trasform a user interface object back to the domain policy class
*
* @param rule the ui object
* @return the domain policy transformed
*/
@Override
public P fromUiModel(U rule)
throws PolicyTransformException {
try {
P domainPolicy = createClass(rule);
if (hasConstructor(domainPolicy.getClass())) {
for (FieldRuleProperty property : rule.getProperties()) {
String field = property.getObjectProperty();
String value = property.getStringValue();
Field f = FieldUtils.getField(domainPolicy.getClass(), field, true);
Object objectValue = convertStringToObject(value, f.getType());
f.set(domainPolicy, objectValue);
}
}
afterFromUiModel(domainPolicy, rule);
return domainPolicy;
} catch (Exception e) {
throw new PolicyTransformException(e);
}
}
private Object getPropertyValue(BaseUiPolicyRule rule, Class<P> domainPolicyClass, PolicyPropertyRef reference) {
for (FieldRuleProperty property : rule.getProperties()) {
String name = property.getName();
if (name.equalsIgnoreCase(reference.name())) {
String field = property.getObjectProperty();
String value = property.getStringValue();
Field f = FieldUtils.getField(domainPolicyClass, field, true);
Object objectValue = convertStringToObject(value, f.getType());
return objectValue;
}
}
return null;
}
private boolean hasConstructor(Class<?> policyClass) {
return policyClass.getConstructors().length > 0;
}
private P createClass(BaseUiPolicyRule rule)
throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException,
IllegalAccessException {
P domainPolicy = null;
String classType = rule.getObjectClassType();
Class<P> domainPolicyClass = (Class<P>) Class.forName(classType);
Constructor constructor = null;
Object[] paramValues = null;
boolean hasConstructor = false;
for (Constructor con : domainPolicyClass.getConstructors()) {
hasConstructor = true;
int parameterSize = con.getParameterTypes().length;
paramValues = new Object[parameterSize];
for (int p = 0; p < parameterSize; p++) {
Annotation[] annotations = con.getParameterAnnotations()[p];
Object paramValue = null;
for (Annotation a : annotations) {
if (a instanceof PolicyPropertyRef) {
// this is the one we want
if (constructor == null) {
constructor = con;
}
//find the value associated to this property
paramValue = getPropertyValue(rule, domainPolicyClass, (PolicyPropertyRef) a);
}
}
paramValues[p] = paramValue;
}
if (constructor != null) {
//exit once we find a constructor with @PropertyRef
break;
}
}
if (constructor != null) {
//call that constructor
try {
domainPolicy = ConstructorUtils.invokeConstructor(domainPolicyClass, paramValues);
} catch (NoSuchMethodException e) {
domainPolicy = domainPolicyClass.newInstance();
}
} else {
//if the class has no public constructor then attempt to call the static instance method
if (!hasConstructor) {
//if the class has a static "instance" method on it then call that
try {
domainPolicy = (P) MethodUtils.invokeStaticMethod(domainPolicyClass, "instance", null);
} catch (NoSuchMethodException | SecurityException | InvocationTargetException e) {
domainPolicy = domainPolicyClass.newInstance();
}
} else {
//attempt to create a new instance
domainPolicy = domainPolicyClass.newInstance();
}
}
return domainPolicy;
}
private List<LabelValue> convertToLabelValue(String[] values) {
if (values != null) {
List<LabelValue> list = new ArrayList<>();
for (String value : values) {
LabelValue labelValue = new LabelValue();
labelValue.setLabel(value);
labelValue.setValue(value);
list.add(labelValue);
}
return list;
}
return null;
}
private LabelValue convertToLabelValue(PropertyLabelValue propertyLabelValue) {
if (propertyLabelValue != null) {
LabelValue labelValue = new LabelValue();
labelValue.setLabel(propertyLabelValue.label());
labelValue.setValue(propertyLabelValue.value());
return labelValue;
}
return null;
}
private List<LabelValue> convertToLabelValue(PropertyLabelValue[] propertyLabelValues) {
List<LabelValue> labelValues = null;
if (propertyLabelValues != null) {
for (PropertyLabelValue propertyLabelValue : propertyLabelValues) {
if (labelValues == null) {
labelValues = new ArrayList<>();
}
LabelValue labelValue = convertToLabelValue((propertyLabelValue));
if (labelValue != null) {
labelValues.add(labelValue);
}
}
}
return labelValues;
}
public Object convertStringToObject(String value, Class type) {
if (type.isEnum()) {
return Enum.valueOf(type, value);
} else if (String.class.equals(type)) {
return value;
}
if (StringUtils.isBlank(value)) {
return null;
} else if (Number.class.equals(type)) {
return NumberUtils.createNumber(value);
} else if (Integer.class.equals(type) || Integer.TYPE.equals(type)) {
return new Integer(value);
} else if (Long.class.equals(type) || Long.TYPE.equals(type)) {
return Long.valueOf(value);
} else if (Double.class.equals(type) || Double.TYPE.equals(type)) {
return Double.valueOf(value);
} else if (Float.class.equals(type) || Float.TYPE.equals(type)) {
return Float.valueOf(value);
} else if (Pattern.class.equals(type)) {
return Pattern.compile(value);
} else if (Boolean.class.equals(type) || Boolean.TYPE.equals(type)) {
return BooleanUtils.toBoolean(value);
} else {
throw new IllegalArgumentException("Unable to convert the value " + value + " to an object of type " + type.getName());
}
}
}