Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow extensible type conversion #430

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Allow type coercion to be configured when building Elide
  • Loading branch information
clayreimann committed Mar 10, 2017
commit 5e1e63a03ad1c0fd57dc4afc85407ae51ea4583a
17 changes: 13 additions & 4 deletions elide-core/src/main/java/com/yahoo/elide/ElideSettingsBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
import com.yahoo.elide.jsonapi.JsonApiMapper;
import com.yahoo.elide.security.PermissionExecutor;
import com.yahoo.elide.security.executors.ActivePermissionExecutor;
import com.yahoo.elide.utils.coerce.converters.ElideConverter;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

/**
Expand All @@ -33,15 +35,17 @@ public class ElideSettingsBuilder {
private final DataStore dataStore;
private AuditLogger auditLogger;
private JsonApiMapper jsonApiMapper;
private EntityDictionary entityDictionary = new EntityDictionary(new HashMap<>());
private Function<RequestScope, PermissionExecutor> permissionExecutorFunction = ActivePermissionExecutor::new;
private List<JoinFilterDialect> joinFilterDialects;
private List<SubqueryFilterDialect> subqueryFilterDialects;
private int defaultMaxPageSize = Pagination.MAX_PAGE_LIMIT;
private int defaultPageSize = Pagination.DEFAULT_PAGE_LIMIT;
private boolean useFilterExpressions;
private int updateStatusCode;

private EntityDictionary entityDictionary = new EntityDictionary(new HashMap<>());
private Function<RequestScope, PermissionExecutor> permissionExecutorFunction = ActivePermissionExecutor::new;
private int defaultMaxPageSize = Pagination.MAX_PAGE_LIMIT;
private int defaultPageSize = Pagination.DEFAULT_PAGE_LIMIT;
private ElideConverter converter = new ElideConverter();

/**
* A new builder used to generate Elide instances. Instantiates an {@link EntityDictionary} without
* providing a mapping of security checks and uses the provided {@link Slf4jLogger} for audit.
Expand Down Expand Up @@ -156,4 +160,9 @@ public ElideSettingsBuilder withUseFilterExpressions(boolean useFilterExpression
this.useFilterExpressions = useFilterExpressions;
return this;
}

public ElideSettingsBuilder withConverters(Map<Integer, ElideConverter.TypeCoercer> converters) {
this.converter = new ElideConverter(converters);
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@

package com.yahoo.elide.utils.coerce.converters;

import com.google.common.collect.Multimaps;
import com.google.common.collect.SortedSetMultimap;
import com.google.common.collect.TreeMultimap;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.commons.beanutils.ConvertUtilsBean;
import org.apache.commons.beanutils.Converter;
import org.apache.commons.lang3.ClassUtils;

import java.util.Date;
import java.util.LinkedHashMap;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;

Expand All @@ -23,17 +28,25 @@ public class ElideConverter extends ConvertUtilsBean {
private static final FromMapConverter FROM_MAP_CONVERTER = new FromMapConverter();
private static final EpochToDateConverter EPOCH_TO_DATE_CONVERTER = new EpochToDateConverter();

private LinkedHashMap<BiFunction<Class<?>, Class<?>, Boolean>, Converter> converters;
public static final BiFunction<Class<?>, Class<?>, Boolean> SOURCE_IS_MAP = (source, target) ->
Map.class.isAssignableFrom(source);
public static final BiFunction<Class<?>, Class<?>, Boolean> TARGET_IS_ENUM = (source, target) ->
target.isEnum();
public static final BiFunction<Class<?>, Class<?>, Boolean> STR_NUM_TO_DATE = (source, target) ->
(String.class.isAssignableFrom(source) || Number.class.isAssignableFrom(source))
&& ClassUtils.isAssignable(target, Date.class);

private static SortedSetMultimap<Integer, TypeCoercer> CONVERTERS = Multimaps.synchronizedSortedSetMultimap(
TreeMultimap.<Integer, TypeCoercer>create());

/**
* Create a new ElideConverter with a set of converters.
*
* @param converters extra converters to consider before the default converters fire
*/
public ElideConverter(LinkedHashMap<BiFunction<Class<?>, Class<?>, Boolean>, Converter> converters) {
// yahoo/elide#260 - enable throwing exceptions when conversion fails
register(true, false, 0);
this.converters = converters;
public ElideConverter(Map<Integer, TypeCoercer> converters) {
register(true, false, 0); // #260 - throw exceptions when conversion fails
converters.forEach((key, value) -> CONVERTERS.put(key, value));
}

/**
Expand All @@ -43,28 +56,41 @@ public ElideConverter() {
this(defaultConverters());
}

@Override
/*
* Overriding lookup to execute enum converter if target is enum
* or map convert if source is map
*/
@Override
public Converter lookup(Class<?> sourceType, Class<?> targetType) {
for (Map.Entry<BiFunction<Class<?>, Class<?>, Boolean>, Converter> entry : converters.entrySet()) {
if (entry.getKey().apply(sourceType, targetType)) {
return entry.getValue();
}
}
return super.lookup(sourceType, targetType);
return CONVERTERS.values().stream()
.filter(tc -> tc.discriminator.apply(sourceType, targetType))
.map(TypeCoercer::getConverter)
.findFirst()
.orElse(super.lookup(sourceType, targetType));
}

public static LinkedHashMap<BiFunction<Class<?>, Class<?>, Boolean>, Converter> defaultConverters() {
LinkedHashMap<BiFunction<Class<?>, Class<?>, Boolean>, Converter> converters = new LinkedHashMap<>();
converters.put((source, target) -> target.isEnum(), TO_ENUM_CONVERTER);
converters.put((source, target) -> Map.class.isAssignableFrom(source), FROM_MAP_CONVERTER);
converters.put(
(source, target) -> (String.class.isAssignableFrom(source) || Number.class.isAssignableFrom(source))
&& ClassUtils.isAssignable(target, Date.class),
EPOCH_TO_DATE_CONVERTER);
public static Map<Integer, TypeCoercer> defaultConverters() {
Map<Integer, TypeCoercer> converters = new HashMap<>();
converters.put(10, new TypeCoercer(TARGET_IS_ENUM, TO_ENUM_CONVERTER));
converters.put(20, new TypeCoercer(SOURCE_IS_MAP, FROM_MAP_CONVERTER));
converters.put(30, new TypeCoercer(STR_NUM_TO_DATE, EPOCH_TO_DATE_CONVERTER));
return converters;
}

/**
* Value type for registering converter with ElideConverter.
*/
@RequiredArgsConstructor
public static class TypeCoercer implements Comparable<TypeCoercer> {
public final BiFunction<Class<?>, Class<?>, Boolean> discriminator;
@Getter public final Converter converter;

@Override
public int compareTo(TypeCoercer o) {
if (this == o) {
return 0;
}
return Integer.compare(hashCode(), o.hashCode());
}
}
}