I'm looking to create a custom interface to inject properties like so...
interface Property<T> { T get(); }
I would like to then set the return value of the get() call using a custom annotation like...
#interface Key { String name(); String fallback() default ""; }
Then uses this throughout my application like...
#key(name = "my.string.property", fallback = "some default value")
Property<String> myStringProperty;
#key(name = "my.number.property", fallback = "1")
Property<Integer> myNumberProperty;
The reason we want to do this rather than using the #Value annotation is to hook these objects into our pre-existing system events with a new PropertyChanged event which can update the return value of the get() method (we will also persist these updates as we're running a distributed system which can create new nodes at anytime) and will also expose these properties in our UIs system admin page.
I've managed to get this working for fields annotated with my custom annotation using ReflectionUtils#doWithFields from my own implementation of BeanPostProcessor#postProcessBeforeInitialization. This is more of a hack as spring does all it's injection and then we're updating the field via reflection so this doesn't work when you annotate the constructor param. I used this guide for that, https://www.baeldung.com/spring-annotation-bean-pre-processor.
so my question is, is there a way to implement a factory object for spring where i can write code to read the annotation and inject an implementation based on that so i don't need to use reflection and it will work no matter where i inject as it'll be a part of the springs normal injection life cycle?
So i found a way to do this implementing the BeanDefinitionRegistryPostProcessor using the org.reflections lib to find all the Key annotations on my Property object.
I was then able create a custom bean definitions for each key which i can then register using the Key as a qualifier to allow spring to inject all my Property objects.
so first thing was adding the Qualifer annotation to my Key annotation.
#Qualifier
#interface Key {
String name();
String fallback() default "";
}
next was to create an implementation of the BeanDefinitionRegistryPostProcessor interface, this registers a bean definition with the concrete implementation of the Property interface to inject at runtime, the constructor parameters and the qualifier from the key annotation found by using the reflections to scan packages
(this was the key to replacing the use of reflections from setting the objects in my bean to just using it to dynamically lookup the key/property and making it available for injection)
#Component
public class PropertyBeanPostProcessor implements BeanDefinitionRegistryPostProcessor {
#Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
#Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
Reflections reflections = new Reflections(ClasspathHelper.forPackage("com.package.to.scan"),
new FieldAnnotationsScanner(), new MethodParameterScanner());
registerBeansForConstructors(registry, reflections.getConstructorsWithAnyParamAnnotated(Key.class));
registerBeansForMethods(registry, reflections.getMethodsWithAnyParamAnnotated(Key.class));
registerBeansForFields(registry, reflections.getFieldsAnnotatedWith(Key.class));
}
private void registerBeansForFields(BeanDefinitionRegistry registry, Set<Field> fields) {
for (Field field : fields) {
Class<?> parameterType = field.getType();
Annotation[] annotations = field.getDeclaredAnnotations();
Type genericType = field.getGenericType();
registerBeanIfPropertyType(registry, parameterType, genericType, annotations);
}
}
private void registerBeansForMethods(BeanDefinitionRegistry registry, Set<Method> methods) {
for (Method method : methods) {
Class<?>[] parameterTypes = method.getParameterTypes();
Annotation[][] annotations = method.getParameterAnnotations();
Type[] genericTypes = method.getGenericParameterTypes();
registerBeansForParameters(registry, parameterTypes, annotations, genericTypes);
}
}
private void registerBeansForConstructors(BeanDefinitionRegistry registry, Set<Constructor> constructors) {
for (Constructor constructor : constructors) {
Class<?>[] parameterTypes = constructor.getParameterTypes();
Annotation[][] annotations = constructor.getParameterAnnotations();
Type[] genericTypes = constructor.getGenericParameterTypes();
registerBeansForParameters(registry, parameterTypes, annotations, genericTypes);
}
}
private void registerBeansForParameters(BeanDefinitionRegistry registry, Class<?>[] parameterTypes, Annotation[][] annotations, Type[] genericTypes) {
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> parameterType = parameterTypes[i];
Annotation[] parameterAnnotations = annotations[i];
Type genericType = genericTypes[i];
registerBeanIfPropertyType(registry, parameterType, genericType, parameterAnnotations);
}
}
private void registerBeanIfPropertyType(BeanDefinitionRegistry registry, Class<?> parameterType, Type genericType, Annotation[] parameterAnnotations) {
if (!Property.class.isAssignableFrom(parameterType)) {
return;
}
Arrays.stream(parameterAnnotations)
.filter(annotation -> Key.class.isAssignableFrom(annotation.annotationType()))
.map(Key.class::cast)
.findFirst()
.ifPresent(key -> register(registry, key, genericType));
}
private void register(BeanDefinitionRegistry registry, Key key, Type type) {
registry.registerBeanDefinition(key.name(), createDefinition(key, type));
log.info("registered property: {}", key);
}
public static BeanDefinition createDefinition(Key key, Type type) {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(PropertyImpl.class);
beanDefinition.setConstructorArgumentValues(createConstructorArgumentValues(key, type));
beanDefinition.addQualifier(createAutowireCandidateQualifier(key));
return beanDefinition;
}
private static AutowireCandidateQualifier createAutowireCandidateQualifier(Key key) {
AutowireCandidateQualifier autowireCandidateQualifier = new AutowireCandidateQualifier(Key.class);
autowireCandidateQualifier.setAttribute("name", key.name());
autowireCandidateQualifier.setAttribute("fallback", key.fallback());
return autowireCandidateQualifier;
}
private static ConstructorArgumentValues createConstructorArgumentValues(Key key, Type type) {
ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
constructorArgumentValues.addIndexedArgumentValue(1, key);
constructorArgumentValues.addIndexedArgumentValue(2, getPropertyType(type));
return constructorArgumentValues;
}
private static Class<?> getPropertyType(Type type) {
if (!(type instanceof ParameterizedType)) {
throw new RuntimeException("field " + type.getTypeName() + " is not parameterised");
}
ParameterizedType parameterizedType = (ParameterizedType) type;
Type[] actualGenericTypeArguments = parameterizedType.getActualTypeArguments();
if (actualGenericTypeArguments.length != 1) {
throw new RuntimeException("invalid number of generics: " + Arrays.stream(actualGenericTypeArguments).map(Type::getTypeName).collect(Collectors.toList()));
}
return TypeToken.of(actualGenericTypeArguments[0]).getRawType();
}
in the end i found this works for me but there may be a better way to create a factory that spring can trigger rather than using the reflections lib as this isn't the fastest
Related
I'm trying to map request parameters of a controller method into a POJO object, but only if any of its fields are present. However, I can't seem to find a way to achieve this. I have the following POJO:
public class TimeWindowModel {
#NotNull
public Date from;
#NotNull
public Date to;
}
If none of the fields are specified, I'd like to get an empty Optional, otherwise I'd get an Optional with a validated instance of the POJO. Spring supports mapping request parameter into POJO objects by leaving them unannotated in the handler:
#GetMapping("/shop/{shopId}/slot")
public Slice<Slot> getSlots(#RequestAttribute("staff") Staff staff,
#PathVariable("shopId") Long shopId, #Valid TimeWindowModel timeWindow) {
// controller code
}
With this, Spring will map request parameters "from" and "to" to an instance of TimeWindowModel. However, I want to make this mapping optional. For POST requests you can use #RequestBody #Valid Optional<T>, which will give you an Optional<T> containing an instance of T, but only if a request body was provided, otherwise it will be empty. This makes #Valid work as expected.
When not annotated, Optional<T> doesn't appear to do anything. You always get an Optional<T> with an instance of the POJO. This is problematic when combined with #Valid because it will complain that "from" and "to" are not set.
The goal is to get either (a) an instance of the POJO where both "from" and "to" are not null or (b) nothing at all. If only one of them is specified, then #Valid should fail and report that the other is missing.
I came up with a solution with a custom HandlerMethodArgumentResolver, Jackson and Jackson Databind.
The annotation:
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
public #interface RequestParamBind {
}
The resolver:
public class RequestParamBindResolver implements HandlerMethodArgumentResolver {
private final ObjectMapper mapper;
public RequestParamBindResolver(ObjectMapper mapper) {
this.mapper = mapper.copy();
this.mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(RequestParamBind.class) != null;
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mav, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// take the first instance of each request parameter
Map<String, String> requestParameters = webRequest.getParameterMap()
.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue()[0]));
// perform the actual resolution
Object resolved = doResolveArgument(parameter, requestParameters);
// *sigh*
// see: https://stackoverflow.com/questions/18091936/spring-mvc-valid-validation-with-custom-handlermethodargumentresolver
if (parameter.hasParameterAnnotation(Valid.class)) {
String parameterName = Conventions.getVariableNameForParameter(parameter);
WebDataBinder binder = binderFactory.createBinder(webRequest, resolved, parameterName);
// DataBinder constructor unwraps Optional, so the target could be null
if (binder.getTarget() != null) {
binder.validate();
BindingResult bindingResult = binder.getBindingResult();
if (bindingResult.getErrorCount() > 0)
throw new MethodArgumentNotValidException(parameter, bindingResult);
}
}
return resolved;
}
private Object doResolveArgument(MethodParameter parameter, Map<String, String> requestParameters) {
Class<?> clazz = parameter.getParameterType();
if (clazz != Optional.class)
return mapper.convertValue(requestParameters, clazz);
// special case for Optional<T>
Type type = parameter.getGenericParameterType();
Class<?> optionalType = (Class<?>)((ParameterizedType)type).getActualTypeArguments()[0];
Object obj = mapper.convertValue(requestParameters, optionalType);
// convert back to a map to find if any fields were set
// TODO: how can we tell null from not set?
if (mapper.convertValue(obj, new TypeReference<Map<String, String>>() {})
.values().stream().anyMatch(Objects::nonNull))
return Optional.of(obj);
return Optional.empty();
}
}
Then, we register it:
#Configuration
public class WebConfig implements WebMvcConfigurer {
//...
#Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new RequestParamBindResolver(new ObjectMapper()));
}
}
Finally, we can use it like so:
#GetMapping("/shop/{shopId}/slot")
public Slice<Slot> getSlots(#RequestAttribute("staff") Staff staff,
#PathVariable("shopId") Long shopId,
#RequestParamBind #Valid Optional<TimeWindowModel> timeWindow) {
// controller code
}
Which works exactly as you'd expect.
I'm sure it's possible to accomplish this by using Spring's own DataBind classes in the resolver. However, Jackson Databind seemed like the most straight-forward solution. That said, it's not able to distinguish between fields that are set to null and fields that just not set. This is not really an issue for my use-case, but it's something that should be noted.
To implement logic (a) both not null or (b) both are nulls you need to implement custom validation.
Examples are here:
https://blog.clairvoyantsoft.com/spring-boot-creating-a-custom-annotation-for-validation-edafbf9a97a4
https://www.baeldung.com/spring-mvc-custom-validator
Generally, you create a new annotation, it's just a stub, and then you create a validator which implements ConstraintValidator where you provide your logic and then you put your new annotation to your POJO.
I have c converter which works:
public class StringToLongConverter implements Converter<String, Long> {
#Override
public Long convert(String source) {
Long myDecodedValue = ...
return myDecodedValue;
}
}
In web configuration I have:
#Override
public void addFormatters (FormatterRegistry registry) {
registry.addConverter(new StringToLongConverter());
}
Everything is good but it works for all controllers and I need it to be executed only for some controllers.
//I need this controller to get myvalue from converter
#RequestMapping(value = "{myvalue}", method = RequestMethod.POST)
public ResponseEntity myvalue1(#PathVariable Long myvalue) {
return new ResponseEntity<>(HttpStatus.OK);
}
//I need this controller to get myvalue without converter
#RequestMapping(value = "{myvalue}", method = RequestMethod.POST)
public ResponseEntity myvalue2(#PathVariable Long myvalue) {
return new ResponseEntity<>(HttpStatus.OK);
}
Can we specify which converters or parameters should be used with custom converter and which should not?
Normally speaking, a registered Converter is bound to an input source and an output destination. In your case <String, Long>. The default Spring converter you used will apply the conversion on each matching source-destination pair.
To gain more control over when to conditionally apply the conversion, a ConditionalGenericConverter can be used. The interface contains 3 methods:
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType), to determine whether the conversion should be applied
Set<ConvertiblePair> getConvertibleTypes() to return a set of source-destination pairs the conversion can be applied to
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) the method in which the actual conversion takes places.
I've set up a small Spring project to play around with the use of a ConditionalGenericConverter:
RequiresConversion.java:
// RequiresConversion is a custom annotation solely used in this example
// to annotate an attribute as "convertable"
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
public #interface RequiresConversion {
}
SomeConverter.java:
#Component
public class SomeConverter implements ConditionalGenericConverter {
#Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
// Verify whether the annotation is present
return targetType.getAnnotation(RequiresConversion.class) != null;
}
#Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(String.class, Long.class));
}
#Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
// Conversion logic here
// In this example it strips "value" from the source string
String sourceValue = ((String) source).replace("value", "");
return Long.valueOf(sourceValue);
}
}
SomeController.java:
#RestController
public class SomeController {
// The path variable used will be converted, resulting in the "value"-prefix
// being stripped in SomeConverter
// Notice the custom '#RequiresConversion' annotation
#GetMapping(value = "/test/{myvalue}")
public ResponseEntity myvalue(#RequiresConversion #PathVariable Long myvalue) {
return new ResponseEntity<>(HttpStatus.OK);
}
// As the #RequiresConversion annotation is not present,
// the conversion is not applied to the #PathVariable
#GetMapping(value = "/test2/{myvalue}")
public ResponseEntity myvalue2(#PathVariable Long myvalue) {
return new ResponseEntity<>(HttpStatus.OK);
}
}
The conversion will occur on http://localhost:8080/test/value123 , resulting in a 123 Long value. However, as the custom annotation #RequiresConversion is not present on the second mapping, the conversion on http://localhost:8080/test2/value123 will be skipped.
You could also inverse the annotation by renaming it to SkipConversion and verifying whether the annotation is absent in the matches() method.
Hope this helps!
I have a question about getting custom annotation value which is value of another custom annotation. For example I have a #SqlInfo annotation interface which have two values which is also annotation interfaces.
SqlInfo.java
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
public #interface SqlInfo {
CodificationInfo codificationInfo();
DocumentInfo documentInfo();
}
#CodificationInfo and #DocumentInfo is also annotation interfaces. Each of it has his own different values.
CodificationInfo.java
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
public #interface CodificationInfo {
enum KEYS {
DOMAIN,
FILE,
TABLE,
CLASS
}
String domain() default "";
String fileName() default "";
String table() default "";
Class codificationClass();
}
While I am using only #CodificationInfo annotation for the class. I am getting values from it by using this method:
Annotation values getter method
public Object getClassAnnotationValue(Class c, String key) {
Annotation annotation = c.getAnnotation(CodificationInfo.class);
return getObjectByKey(annotation, key);
}
private Object getObjectByKey(Annotation annotation, String key) {
if (annotation instanceof CodificationInfo) {
if (key.equalsIgnoreCase(CodificationInfo.KEYS.TABLE.toString())) {
return ((CodificationInfo) annotation).table();
} else if (key.equalsIgnoreCase(CodificationInfo.KEYS.CLASS.toString())) {
return ((CodificationInfo) annotation).codificationClass();
} else if (key.equalsIgnoreCase(CodificationInfo.KEYS.DOMAIN.toString())) {
return ((CodificationInfo) annotation).domain();
} else if (key.equalsIgnoreCase(CodificationInfo.KEYS.FILE.toString())) {
return ((CodificationInfo) annotation).fileName();
}
}
return null;
}
I want to know how to get #CodificationInfo values while I am using #SqlInfo annotation for the class? It means - how to get values from sub-annotation?
P.S.: I know that I can use both annotations separately for the class. But I want to know the any way how to get values from sub-annotation. For example hibernate use it for #AuditOverrides annotation.
If you have a type declared like:
#SqlInfo(codificationInfo = #CodificationInfo(codificationClass = AClass.class)
public class MyType { }
you can reflectively get the inner annotation values with:
final SqlInfo sqlInfoAnnotation = (SqlInfo) c.getAnnotation(SqlInfo.class);
if (sqlInfoAnnotation == null) return;
final CodificationInfo codInfoAnnotation = sqlInfoAnnotation.codificationInfo();
final Class<?> codClass = codInfoAnnotation.codificationClass();
Note: you can avoid having to cast the annotation by not using raw types (prefer Class<?> over Class).
Is there a way to get Spring AOP to recognize the value of an argument that has been annotated? (There is no guarantee in the order of the arguments passed into the aspect, so I'm hoping to use an annotation to mark the parameter that needs to be used to process the aspect)
Any alternative approaches would also be extremely helpful.
#Target({ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
public #interface Wrappable {
}
#Target({ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
public #interface Key {
}
#Wrappable
public void doSomething(Object a, #Key Object b) {
// something
}
#Aspect
#Component
public class MyAspect {
#After("#annotation(trigger)" /* what can be done to get the value of the parameter that has been annotated with #Key */)
public void trigger(JoinPoint joinPoint, Trigger trigger) { }
Here is an example of an aspect class which should process a method tagged with #Wrappable annotation. Once the wrapper method is called, you can iterate over method parameters to find out if any parameter is tagged with the #Key annotation. The keyParams list contains any parameter tagged with a #Key annotation.
#Aspect
#Component
public class WrappableAspect {
#After("#annotation(annotation) || #within(annotation)")
public void wrapper(
final JoinPoint pointcut,
final Wrappable annotation) {
Wrappable anno = annotation;
List<Parameter> keyParams = new ArrayList<>();
if (annotation == null) {
if (pointcut.getSignature() instanceof MethodSignature) {
MethodSignature signature =
(MethodSignature) pointcut.getSignature();
Method method = signature.getMethod();
anno = method.getAnnotation(Wrappable.class);
Parameter[] params = method.getParameters();
for (Parameter param : params) {
try {
Annotation keyAnno = param.getAnnotation(Key.class);
keyParams.add(param);
} catch (Exception e) {
//do nothing
}
}
}
}
}
}
We cannot get the parameter annotation value as an argument to AOP like we are doing it for the method annotation because the annotation is not an actual parameter and in there you can only reference actual arguments.
args(#Key b)
This annotation will give you the value of Object(b) not the value of #Key annotation.
We can do in this way to get the value of the parameter annotation:
MethodSignature methodSig = (MethodSignature) joinpoint.getSignature();
Annotation[][] annotations = methodSig.getMethod().getParameterAnnotations();
if (annotations != null) {
for (Annotation[] annotArr : annotations) {
for (Annotation annot : annotArr) {
if (annot instanceof KeyAnnotation) {
System.out.println(((KeyAnnotation) annot).value());
}
}
}
}
We're developing a RESTful API using Java EE 7 (RESTEasy / Hibernate / Jackson).
We want the API to serialize all child entities using their IDs, by default. We're doing this mostly to maintain consistency with our deserialization strategy, where we insist on receiving an ID.
However, we also want our users to be able to choose to get an expanded view of any of our child entities, either through a custom endpoint or a query parameter (undecided). For example:
// http://localhost:8080/rest/operator/1
// =====================================
{
"operatorId": 1,
"organization": 34,
"endUser": 23
}
// http://localhost:8080/rest/operator/1?expand=organization
// =====================================
{
"operatorId": 1,
"organization": {
"organizationId": 34,
"organizationName": "name"
},
"endUser": 23
}
// http://localhost:8080/rest/operator/1?expand=enduser
// =====================================
{
"operatorId": 1,
"organization": 34,
"endUser": {
"endUserId": 23,
"endUserName": "other name"
}
}
// http://localhost:8080/rest/operator/1?expand=organization,enduser
// =====================================
{
"operatorId": 1,
"organization": {
"organizationId": 34,
"organizationName": "name"
},
"endUser": {
"endUserId": 23,
"endUserName": "other name"
}
}
Is there a way to dynamically change the behavior of Jackson to determine whether a specified AbstractEntity field is serialized in full form or as its ID? How might it be done?
Additional Info
We know of a few ways to serialize our child entities using their IDs, including:
public class Operator extends AbstractEntity {
...
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="organizationId")
#JsonIdentityReference(alwaysAsId=true)
public getOrganization() { ... }
...
}
and
public class Operator extends AbstractEntity {
...
#JsonSerialize(using=AbstractEntityIdSerializer.class)
public getOrganization() { ... }
...
}
where AbstractEntityIdSerializer serializes the entity using its ID.
The problem is that we don't know of a way for the user to override that default behavior and revert to standard Jackson object serialization. Ideally they'd also be able to choose which child properties to serialize in full form.
It would be awesome to dynamically toggle the alwaysAsId argument of #JsonIdentityReference for any property at runtime, if that's possible, or make the equivalent change to ObjectMapper/ObjectWriter.
Update: Working(?) Solution
We haven't had a chance to fully test this yet, but I've been working on a solution that leverages overriding Jackson's AnnotationIntrospector class. It seems to be working as intended.
public class CustomAnnotationIntrospector extends JacksonAnnotationIntrospector {
private final Set<String> expandFieldNames_;
public CustomAnnotationIntrospector(Set<String> expandFieldNames) {
expandFieldNames_ = expandFieldNames;
}
#Override
public ObjectIdInfo findObjectReferenceInfo(Annotated ann, ObjectIdInfo objectIdInfo) {
JsonIdentityReference ref = _findAnnotation(ann, JsonIdentityReference.class);
if (ref != null) {
for (String expandFieldName : expandFieldNames_) {
String expandFieldGetterName = "get" + expandFieldName;
String propertyName = ann.getName();
boolean fieldNameMatches = expandFieldName.equalsIgnoreCase(propertyName);
boolean fieldGetterNameMatches = expandFieldGetterName.equalsIgnoreCase(propertyName);
if (fieldNameMatches || fieldGetterNameMatches) {
return objectIdInfo.withAlwaysAsId(false);
}
}
objectIdInfo = objectIdInfo.withAlwaysAsId(ref.alwaysAsId());
}
return objectIdInfo;
}
}
At serialization time, we copy our ObjectMapper (so the AnnotationIntrospector runs again) and apply CustomAnnotationIntrospector as follows:
#Context
private HttpRequest httpRequest_;
#Override
writeTo(...) {
// Get our application's ObjectMapper.
ContextResolver<ObjectMapper> objectMapperResolver = provider_.getContextResolver(ObjectMapper.class,
MediaType.WILDCARD_TYPE);
ObjectMapper objectMapper = objectMapperResolver.getContext(Object.class);
// Get Set of fields to be expanded (pre-parsed).
Set<String> fieldNames = (Set<String>)httpRequest_.getAttribute("ExpandFields");
if (!fieldNames.isEmpty()) {
// Pass expand fields to AnnotationIntrospector.
AnnotationIntrospector expansionAnnotationIntrospector = new CustomAnnotationIntrospector(fieldNames);
// Replace ObjectMapper with copy of ObjectMapper and apply custom AnnotationIntrospector.
objectMapper = objectMapper.copy();
objectMapper.setAnnotationIntrospector(expansionAnnotationIntrospector);
}
ObjectWriter objectWriter = objectMapper.writer();
objectWriter.writeValue(...);
}
Any glaring flaws in this approach? It seems relatively straightforward and is fully dynamic.
The answer is Jackson's mixin feature:
You create a simple Java class that has the exact same method signature as the anotated method of the entity. You annotate that method with the modified value. the body of the method is insignificant (it would not be called):
public class OperatorExpanded {
...
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="organizationId")
#JsonIdentityReference(alwaysAsId=false)
public Organization getOrganization() { return null; }
...
}
you tie the mixin to the entity-to-be-serialized using Jackson's module system: this can be decided at run time
ObjectMapper mapper = new ObjectMapper();
if ("organization".equals(request.getParameter("exapnd")) {
SimpleModule simpleModule = new SimpleModule();
simpleModule.setMixInAnnotation(Operator.class, OperatorExpanded.class);
mapper.registerModule(simpleModule);
}
now, the mapper will take the annotations from the mixin, but invoke the method of the entity.
If you are looking for a generalized solution that needs to be extended to all of your resources you may try following approach. I tried below solution using Jersey and Jackson. It should also work with RestEasy.
Basically, you need to write a custom jackson provider which set a special serializer for an expand field. Also, you need to pass the expand fields to the serializer so that you can decide how to do the serialization for expand fields.
#Singleton
public class ExpandFieldJacksonProvider extends JacksonJaxbJsonProvider {
#Inject
private Provider<ContainerRequestContext> provider;
#Override
protected JsonEndpointConfig _configForWriting(final ObjectMapper mapper, final Annotation[] annotations, final Class<?> defaultView) {
final AnnotationIntrospector customIntrospector = mapper.getSerializationConfig().getAnnotationIntrospector();
// Set the custom (user) introspector to be the primary one.
final ObjectMapper filteringMapper = mapper.setAnnotationIntrospector(AnnotationIntrospector.pair(customIntrospector, new JacksonAnnotationIntrospector() {
#Override
public Object findSerializer(Annotated a) {
// All expand fields should be annotated with '#ExpandField'.
ExpandField expField = a.getAnnotation(ExpandField.class);
if (expField != null) {
// Use a custom serializer for expand field
return new ExpandSerializer(expField.fieldName(), expField.idProperty());
}
return super.findSerializer(a);
}
}));
return super._configForWriting(filteringMapper, annotations, defaultView);
}
#Override
public void writeTo(final Object value, final Class<?> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType, final MultivaluedMap<String, Object> httpHeaders,
final OutputStream entityStream) throws IOException {
// Set the expand fields to java's ThreadLocal so that it can be accessed in 'ExpandSerializer' class.
ExpandFieldThreadLocal.set(provider.get().getUriInfo().getQueryParameters().get("expand"));
super.writeTo(value, type, genericType, annotations, mediaType, httpHeaders, entityStream);
// Once the serialization is done, clear ThreadLocal
ExpandFieldThreadLocal.remove();
}
ExpandField.java
#Retention(RUNTIME)
public #interface ExpandField {
// name of expand field
String fieldName();
// name of Id property in expand field. For eg: oraganisationId
String idProperty();
}
ExpandFieldThreadLocal.java
public class ExpandFieldThreadLocal {
private static final ThreadLocal<List<String>> _threadLocal = new ThreadLocal<>();
public static List<String> get() {
return _threadLocal.get();
}
public static void set(List<String> expandFields) {
_threadLocal.set(expandFields);
}
public static void remove() {
_threadLocal.remove();
}
}
ExpandFieldSerializer.java
public static class ExpandSerializer extends JsonSerializer<Object> {
private String fieldName;
private String idProperty;
public ExpandSerializer(String fieldName,String idProperty) {
this.fieldName = fieldName;
this.idProperty = idProperty;
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
// Get expand fields in current request which is set in custom jackson provider.
List<String> expandFields = ExpandFieldThreadLocal.get();
if (expandFields == null || !expandFields.contains(fieldName)) {
try {
// If 'expand' is not present in query param OR if the 'expand' field does not contain this field, write only id.
serializers.defaultSerializeValue(value.getClass().getMethod("get"+StringUtils.capitalize(idProperty)).invoke(value),gen);
} catch (Exception e) {
//Handle Exception here
}
} else {
serializers.defaultSerializeValue(value, gen);
}
}
}
Operator.java
public class Operator extends AbstractEntity {
...
#ExpandField(fieldName = "organization",idProperty="organizationId")
private organization;
...
}
The final step is to register the new ExpandFieldJacksonProvider. In Jersey, we register it through an instance of javax.ws.rs.core.Application as shown below. I hope there is something similar in RestEasy. By default, most of the JAX-RS libraries tend to load default JacksonJaxbJsonProvider through auto-discovery. You have to make sure auto-discovery is disabled for Jackson and new ExpandFieldJacksonProvider is registered.
public class JaxRsApplication extends Application{
#Override
public Set<Class<?>> getClasses() {
Set<Class<?>> clazzes=new HashSet<>();
clazzes.add(ExpandFieldJacksonProvider.class);
return clazzes;
}
}