I need to send a POST request with duplicate parameter names, like "param=a¶m=b".
Overriding the Request.getParams() does not work since Map cannot have duplicate keys, so only one value would be sent.
I know I can rewrite the Request class to use a Map or Map>, but I was wandering if there is any other way that would not require altering the library.
Thanks in advance.
PS: I have filed the same question on the volley-users group: https://groups.google.com/forum/#!topic/volley-users/tFRclnEbpAk
Ficus Kirkpatrick answered my question on the volley-users group
(https://groups.google.com/d/msg/volley-users/tFRclnEbpAk/uiC2f9nAIgkJ):
You can override getBody() without having to modify the library.
F
So I created the following helper class:
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class HttpParams extends HashMap<String, List<String>> {
private static final long serialVersionUID = 1L;
public HttpParams() {
super();
}
public HttpParams(int capacity) {
super(capacity);
}
public HttpParams(Map<String, List<String>> map) {
super(map);
}
public HttpParams(int capacity, float loadFactor) {
super(capacity, loadFactor);
}
/*
* This is the method to use for adding post parameters
*/
public void add(String key, String value) {
if (containsKey(key)) {
get(key).add(value);
}
else {
ArrayList<String> list = new ArrayList<String>();
list.add(value);
put(key, list);
}
}
/**
* Converts the Map into an application/x-www-form-urlencoded encoded string.
*/
public byte[] encodeParameters(String paramsEncoding) {
StringBuilder encodedParams = new StringBuilder();
try {
for (Map.Entry<String, List<String>> entry : entrySet()) {
String key = URLEncoder.encode(entry.getKey(), paramsEncoding);
for (String value : entry.getValue()) {
encodedParams.append(key);
encodedParams.append('=');
encodedParams.append(URLEncoder.encode(value, paramsEncoding));
encodedParams.append('&');
}
}
return encodedParams.toString().getBytes(paramsEncoding);
} catch (UnsupportedEncodingException uee) {
throw new RuntimeException("Encoding not supported: " + paramsEncoding, uee);
}
}
}
and then in my class that extends Request I overrided the getBody():
#Override
public byte[] getBody() throws AuthFailureError {
if (mParams != null && mParams.size() > 0) {
return mParams.encodeParameters(getParamsEncoding());
}
return null;
}
hey I just answered you in the google group question but I Thought I'd also post it here just in case someone came here first..
It is true that Map does not support duplicate but however that you
could do something like this. you won't have to override getBody(), just the getParams which i'm assuming you're already doing.
#Override
protected Map<String, String> getParams() {
Map<String, String> params = new HashMap<String, String>();;
params.putAll(AddArrayParams());
return params;
}
public Map<? extends String, ? extends String> AddArrayParams() {
Map<String, String> params = new HashMap<String, String>();
// figure that if its an array and the data is sent as [0],[1] then lets just send it up that way
params.put("param[0]","a");
params.put("param[1]","b");
params.put("param[3]","c");
////etc
return params;
}
Good Luck
Related
I am trying to de-serialize the json string. I tried different API's but I didn't find the solution. Here, am trying to deserialize below json and want to read value of each field/element. Example below -
String inputJson = "{"phone":null, "address":"underworld"}";
LinkedTreeMap map = new Gson().fromJson(inputJson , LinkedTreeMap.class);
When I say map.containsKey("phone), it is giving as false, it means "phone" element is not present in the json string. But, this is not correct as we could see that this element is present in the input json.
Can anyone help me on any API which can give keys with value as well.
With spring boot what is the correct jackson deserialization configuration which can accept null values? Currently I am using like below -
pubic ObjectMapper objectMapper(Jckson3OjectMapperBuilder builder) {
ObjectMapper mapper = builder.build();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper;
}
I've written some tests that deserialize and serialize your cases maybe this will help you
GSON always deserializes null object if you want to change, write your adapter
I use JDK1.8 and com.google.code.gson:gson:2.8.6
package pl.jac.mija.gson;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.internal.LinkedTreeMap;
import com.google.gson.internal.bind.ObjectTypeAdapter;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import org.junit.Test;
import sun.reflect.generics.reflectiveObjects.NotImplementedException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class GsonWithNullTest {
#Test
public void deserializeWithNull() {
//given
String inputJson = "{\"phone\":null, \"address\":\"underworld\"}";
//when
LinkedTreeMap<String, Object> map = new Gson().fromJson(inputJson, LinkedTreeMap.class);
boolean phone = map.containsKey("phone");
//then
assertEquals(true, phone);
}
#Test
public void deserializeWithoutNull_V1_use_adapter() {
//given
String inputJson = "{\"phone\":null, \"address\":\"underworld\"}";
//when
Gson gson = new GsonBuilder().registerTypeAdapter(LinkedTreeMap.class, new MyAdapterSkipNull()).create();
LinkedTreeMap<String, Object> map = gson.fromJson(inputJson, LinkedTreeMap.class);
//then
boolean isPhone = map.containsKey("phone");
boolean isAddress = map.containsKey("address");
assertEquals(false, isPhone);
assertEquals(true, isAddress);
}
#Test
public void deserializeWithoutNull_V2_use_post_filter_null() {
//given
String inputJson = "{\"phone\":null, \"address\":\"underworld\"}";
//when
Gson gson = new GsonBuilder().registerTypeAdapter(LinkedTreeMap.class, new MyAdapterSkipNull()).create();
LinkedTreeMap<String, Object> map = new Gson().fromJson(inputJson, LinkedTreeMap.class);
Map<String, Object> collect = map.entrySet().stream().filter(x -> x.getValue() != null).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
//then
boolean isPhone = collect.containsKey("phone");
boolean isAddress = collect.containsKey("address");
assertEquals(false, isPhone);
assertEquals(true, isAddress);
}
#Test
public void serializeWithoutNull() {
//given
Map<String, Object> map = new HashMap<>();
map.put("phone", null);
map.put("address", "underworld");
//when
Gson gson = new GsonBuilder().serializeNulls().create();
String json = gson.toJson(map);
//then
List<String> answert = new ArrayList<>();
answert.add("{\"address\":\"underworld\",\"phone\":null}");
answert.add("{\"phone\":null,\"address\":\"underworld\"}");
assertTrue(answert.contains(json));
}
#Test
public void serializeWithNull() {
//given
Map<String, Object> map = new HashMap<>();
map.put("phone", null);
map.put("address", "underworld");
//when
Gson gson = new Gson();
String json = gson.toJson(map);
//then
assertEquals("{\"address\":\"underworld\"}", json);
}
}
class MyAdapterSkipNull extends TypeAdapter<LinkedTreeMap<String, Object>> {
#Override
public void write(JsonWriter out, LinkedTreeMap<String, Object> value) throws IOException {
throw new NotImplementedException();
}
#Override
public LinkedTreeMap<String, Object> read(JsonReader in) throws IOException {
JsonToken peek = in.peek();
if (peek == JsonToken.NULL) {
in.nextNull();
return null;
}
TypeAdapter<Object> objectTypeAdapter = ObjectTypeAdapter.FACTORY.create(new Gson(), TypeToken.get(Object.class));
LinkedTreeMap<String, Object> map = new LinkedTreeMap<>();
in.beginObject();
while (in.hasNext()) {
String key = in.nextName();
JsonToken peek1 = in.peek();
if (JsonToken.NULL.equals(peek1)) {
in.skipValue(); //skip NULL
} else {
Object read = objectTypeAdapter.read(in);
map.put(key, read);
}
}
in.endObject();
return map;
}
}
I have solved this problem by changing spring boot end point method argument signature from Object to String. Earlier it was Object type because of that it just ignoring keys having null values in the String. And in the controller I am checking the existence of the key as below -
public ResponseEntity<Object> validate(#RequestBody String requestBody) {
Object requestObject = new ObjectMapper().readValue(requestBody, Object.class);
LinkedTreeMap requestObjectMap = new Gson().fromJson(requestObject.toString(), LinkedTreeMap.class);
List<FieldError> fieldErrors = new ArrayList<>();
final boolean isKeyExists = requestObjectMap.containsKey("keyname");
final Object fieldValue = requestObjectMap.get(optionalField);
if (isKeyExists && (Objects.isNull(fieldValue)) {
System.out.println("Key exists but its value is null in the input Json request");
}
// other logic
}
I have a spring boot app, and I want to send DTO validation constraints as well as field value to the client.
Having DTO
class PetDTO {
#Length(min=5, max=15)
String name;
}
where name happens to be 'Leviathan', should result in this JSON being sent to client:
{
name: 'Leviathan'
name_constraint: { type: 'length', min:5, max: 15},
}
Reasoning is to have single source of truth for validations. Can this be done with reasonable amount of work?
To extend Frederik's answer I'll show a little sample code that convers an object to map and serializes it.
So here is the User pojo:
import org.hibernate.validator.constraints.Length;
public class User {
private String name;
public User(String name) {
this.name = name;
}
#Length(min = 5, max = 15)
public String getName() {
return name;
}
}
Then the actual serializer:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.util.ReflectionUtils;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toMap;
public class UserSerializer extends StdSerializer<User> {
public UserSerializer(){
this(User.class);
}
private UserSerializer(Class t) {
super(t);
}
#Override
public void serialize(User bean, JsonGenerator gen, SerializerProvider provider) throws IOException {
Map<String, Object> properties = beanProperties(bean);
gen.writeStartObject();
for (Map.Entry<String, Object> entry : properties.entrySet()) {
gen.writeObjectField(entry.getKey(), entry.getValue());
}
gen.writeEndObject();
}
private static Map<String, Object> beanProperties(Object bean) {
try {
return Arrays.stream(Introspector.getBeanInfo(bean.getClass(), Object.class).getPropertyDescriptors())
.filter(descriptor -> Objects.nonNull(descriptor.getReadMethod()))
.flatMap(descriptor -> {
String name = descriptor.getName();
Method getter = descriptor.getReadMethod();
Object value = ReflectionUtils.invokeMethod(getter, bean);
Property originalProperty = new Property(name, value);
Stream<Property> constraintProperties = Stream.of(getter.getAnnotations())
.map(anno -> new Property(name + "_constraint", annotationProperties(anno)));
return Stream.concat(Stream.of(originalProperty), constraintProperties);
})
.collect(toMap(Property::getName, Property::getValue));
} catch (Exception e) {
return Collections.emptyMap();
}
}
// Methods from Annotation.class
private static List<String> EXCLUDED_ANNO_NAMES = Arrays.asList("toString", "equals", "hashCode", "annotationType");
private static Map<String, Object> annotationProperties(Annotation anno) {
try {
Stream<Property> annoProps = Arrays.stream(Introspector.getBeanInfo(anno.getClass(), Proxy.class).getMethodDescriptors())
.filter(descriptor -> !EXCLUDED_ANNO_NAMES.contains(descriptor.getName()))
.map(descriptor -> {
String name = descriptor.getName();
Method method = descriptor.getMethod();
Object value = ReflectionUtils.invokeMethod(method, anno);
return new Property(name, value);
});
Stream<Property> type = Stream.of(new Property("type", anno.annotationType().getName()));
return Stream.concat(type, annoProps).collect(toMap(Property::getName, Property::getValue));
} catch (IntrospectionException e) {
return Collections.emptyMap();
}
}
private static class Property {
private String name;
private Object value;
public Property(String name, Object value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public Object getValue() {
return value;
}
}
}
And finally we need to register this serializer to be used by Jackson:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
#SpringBootApplication(scanBasePackages = "sample.spring.serialization")
public class SerializationApp {
#Bean
public Jackson2ObjectMapperBuilder mapperBuilder(){
Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder = new Jackson2ObjectMapperBuilder();
jackson2ObjectMapperBuilder.serializers(new UserSerializer());
return jackson2ObjectMapperBuilder;
}
public static void main(String[] args) {
SpringApplication.run(SerializationApp.class, args);
}
}
#RestController
class SerializationController {
#GetMapping("/user")
public User user() {
return new User("sample");
}
}
The Json that will be emitted:
{
"name_constraint":{
"min":5,
"max":15,
"payload":[],
"groups":[],
"message":"{org.hibernate.validator.constraints.Length.message}",
"type":"org.hibernate.validator.constraints.Length"
},
"name":"sample"
}
Hope this helps. Good luck.
You can always use a custom Jackson Serializer for this. Plenty of docs to do this can be found on the internet, might look something like this:
public void serialize(PetDTO value, JsonGenerator jgen, ...) {
jgen.writeStartObject();
jgen.writeNumberField("name", value.name);
jgen.writeObjectField("name_consteaint", getConstraintValue(value));
}
public ConstaintDTO getConstraintValue(PetDTO value) {
// Use reflection to check if the name field on the PetDTO is annotated
// and extract the min, max and type values from the annotation
return new ConstaintDTO().withMaxValue(...).withMinValue(...).ofType(...);
}
You may want to create a base-DTO class for which the converter kicks in so you don't have to create a custom converter for all your domain objects that need to expose the constraints.
By combining reflection and smart use of writing fields, you can get close. Downside is you can't take advantage of the #JsonXXX annotations on your domain objects, since you're writing the JSON yourself.
More ideal solution whould be to have Jackson convert, but have some kind of post-conversion-call to add additional XX_condtion properties to the object. Maybe start by overriding the default object-serializer (if possible)?
Feign default expander to convert param:
final class ToStringExpander implements Expander {
#Override
public String expand(Object value) {
return value.toString();
}
}
I want custom it to convert user to support GET param, like this
#FeignClient("xx")
interface UserService{
#RequestMapping(value="/users",method=GET)
public List<User> findBy(#ModelAttribute User user);
}
userService.findBy(user);
What can i do?
First,you must write a expander like ToJsonExpander:
public class ToJsonExpander implements Param.Expander {
private static ObjectMapper objectMapper = new ObjectMapper();
public String expand(Object value) {
try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException e) {
throw new ExpanderException(e);
}
}
}
Second, write a AnnotatedParameterProcessor like JsonArgumentParameterProcessor to add expander for your processor.
public class JsonArgumentParameterProcessor implements AnnotatedParameterProcessor {
private static final Class<JsonArgument> ANNOTATION = JsonArgument.class;
public Class<? extends Annotation> getAnnotationType() {
return ANNOTATION;
}
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation) {
MethodMetadata data = context.getMethodMetadata();
String name = ANNOTATION.cast(annotation).value();
String method = data.template().method();
Util.checkState(Util.emptyToNull(name) != null,
"JsonArgument.value() was empty on parameter %s", context.getParameterIndex());
context.setParameterName(name);
if (method != null && (HttpMethod.POST.matches(method) || HttpMethod.PUT.matches(method) || HttpMethod.DELETE.matches(method))) {
data.formParams().add(name);
} else {
`data.indexToExpanderClass().put(context.getParameterIndex(), ToJsonExpander.class);`
Collection<String> query = context.setTemplateParameter(name, data.template().queries().get(name));
data.template().query(name, query);
}
return true;
}
}
Third,add it to Feign configuration.
#Bean
public Contract feignContract(){
List<AnnotatedParameterProcessor> processors = new ArrayList<>();
processors.add(new JsonArgumentParameterProcessor());
processors.add(new PathVariableParameterProcessor());
processors.add(new RequestHeaderParameterProcessor());
processors.add(new RequestParamParameterProcessor());
return new SpringMvcContract(processors);
}
Now, you can use #JsonArgument to send model argument like:
public void saveV10(#JsonArgument("session") Session session);
I don't know what #ModelAttribute does but I was looking for a way to convert #RequestParam values so I did this:
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
import org.springframework.cloud.netflix.feign.FeignFormatterRegistrar;
import org.springframework.format.FormatterRegistry;
import org.springframework.stereotype.Component;
import static com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat.E164;
#Component
public class PhoneNumberFeignFormatterRegistrar implements FeignFormatterRegistrar {
private final PhoneNumberUtil phoneNumberUtil;
public PhoneNumberFeignFormatterRegistrar(PhoneNumberUtil phoneNumberUtil) {
this.phoneNumberUtil = phoneNumberUtil;
}
#Override
public void registerFormatters(FormatterRegistry registry) {
registry.addConverter(Phonenumber.PhoneNumber.class, String.class, source -> phoneNumberUtil.format(source, E164));
}
}
Now stuff like the following works
import com.google.i18n.phonenumbers.Phonenumber;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.hateoas.Resource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
#FeignClient("data-service")
public interface DataClient {
#RequestMapping(method = RequestMethod.GET, value = "/phoneNumbers/search/findByPhoneNumber")
Resource<PhoneNumberRecord> getPhoneNumber(#RequestParam("phoneNumber") Phonenumber.PhoneNumber phoneNumber);
}
As the open feign issue and spring doc say:
The OpenFeign #QueryMap annotation provides support for POJOs to be used as GET parameter maps.
Spring Cloud OpenFeign provides an equivalent #SpringQueryMap annotation, which is used to annotate a POJO or Map parameter as a query parameter map since 2.1.0.
You can use it like this:
#GetMapping("user")
String getUser(#SpringQueryMap User user);
public class User {
private String name;
private int age;
...
}
for (Person person : company.getPersons()) {
resource.add(linkTo(methodOn(PersonController.class).view(person.getId()))
.withRel("persons"));
}
I want to return an array of links by "persons" rel. It's all ok if I have multiple persons, but if I have only a single person it returns a single element and my client code that expects array fails.
not possible in spring hateoas 18. We overloaded the built in serializer to account for this. It was very nasty.
Technically a client should interpret rel : {} as rel : [{}] to be HAL compliant..but they rarely do..
you have to remove and override the built in HATEOAS one, we did it like this, but this effectively removes all other converters:
#Configuration
public class WebMVCConfig extends WebMvcConfigurerAdapter {
private static final String DELEGATING_REL_PROVIDER_BEAN_NAME = "_relProvider";
private static final String LINK_DISCOVERER_REGISTRY_BEAN_NAME = "_linkDiscovererRegistry";
private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";
public WebMVCConfig(){
}
#Autowired
private ListableBeanFactory beanFactory;
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//Need to override some behaviour in the HAL Serializer...so let's do that
CurieProvider curieProvider = getCurieProvider(beanFactory);
RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
halObjectMapper.registerModule(new MultiLinkAwareJackson2HalModule());
halObjectMapper.setHandlerInstantiator(new MultiLinkAwareJackson2HalModule.MultiLinkAwareHalHandlerInstantiator(relProvider, curieProvider));
MappingJackson2HttpMessageConverter halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class);
halConverter.setSupportedMediaTypes(Arrays.asList(HAL_JSON));
halConverter.setObjectMapper(halObjectMapper);
converters.add(halConverter);
}
private static CurieProvider getCurieProvider(BeanFactory factory) {
try {
return factory.getBean(CurieProvider.class);
} catch (NoSuchBeanDefinitionException e) {
return null;
}
}
overriding the serializer is really ugly business..maybe we should have just built a new one from scratch
/*
* Copyright 2012-2014 the original author or authors.
*
* 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.
*/
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.ser.std.MapSerializer;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.google.common.collect.ImmutableSet;
import org.springframework.hateoas.hal.*;
import java.io.IOException;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.ResourceSupport;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializerProvider;
import javax.xml.bind.annotation.XmlElement;
/**
* Jackson 2 module implementation to render {#link org.springframework.hateoas.Link} and {#link org.springframework.hateoas.ResourceSupport} instances in HAL compatible JSON.
*
* Extends this class to make it possible for a relationship to be serialized as an array even if there is only 1 link
* This is done is in OptionalListJackson2Serializer::serialize method.
*
* Relationships to force as arrays are defined in relsToForceAsAnArray
*/
public class MultiLinkAwareJackson2HalModule extends Jackson2HalModule {
private static final long serialVersionUID = 7806951456457932384L;
private static final ImmutableSet<String> relsToForceAsAnArray = ImmutableSet.copyOf(Arrays.asList(
ContractConstants.REL_PROMOTION_TARGET,
ContractConstants.REL_PROFILE,
ContractConstants.REL_IMAGE_FLAG,
ContractConstants.REL_IMAGE,
ContractConstants.REL_IMAGE_PRIMARY,
ContractConstants.REL_IMAGE_SECONDARY,
ContractConstants.REL_IMAGE_MENU,
ContractConstants.REL_ITEM
));
private static abstract class MultiLinkAwareResourceSupportMixin extends ResourceSupport {
#Override
#XmlElement(name = "link")
#JsonProperty("_links")
//here's the only diff from org.springframework.hateoas.hal.ResourceSupportMixin
//we use a different HalLinkListSerializer
#JsonSerialize(include = JsonSerialize.Inclusion.NON_EMPTY, using = MultiLinkAwareHalLinkListSerializer.class)
#JsonDeserialize(using = MultiLinkAwareJackson2HalModule.HalLinkListDeserializer.class)
public abstract List<Link> getLinks();
}
public MultiLinkAwareJackson2HalModule() {
super();
//NOTE: super calls setMixInAnnotation(Link.class, LinkMixin.class);
//you must not override this as this is how Spring-HATEOAS determines if a
//Hal converter has been registered for not.
//If it determines a Hal converter has not been registered, it will register it's own
//that will override this one
//Use customized ResourceSupportMixin to use our LinkListSerializer
setMixInAnnotation(ResourceSupport.class, MultiLinkAwareResourceSupportMixin.class);
}
public static class MultiLinkAwareHalLinkListSerializer extends Jackson2HalModule.HalLinkListSerializer {
private final BeanProperty property;
private final CurieProvider curieProvider;
private final Set<String> relsAsMultilink;
public MultiLinkAwareHalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, Set<String> relsAsMultilink) {
super(property, curieProvider);
this.property = property;
this.curieProvider = curieProvider;
this.relsAsMultilink = relsAsMultilink;
}
#Override
public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonGenerationException {
// sort links according to their relation
Map<String, List<Object>> sortedLinks = new LinkedHashMap<String, List<Object>>();
List<Link> links = new ArrayList<Link>();
boolean prefixingRequired = curieProvider != null;
boolean curiedLinkPresent = false;
for (Link link : value) {
String rel = prefixingRequired ? curieProvider.getNamespacedRelFrom(link) : link.getRel();
if (!link.getRel().equals(rel)) {
curiedLinkPresent = true;
}
if (sortedLinks.get(rel) == null) {
sortedLinks.put(rel, new ArrayList<Object>());
}
links.add(link);
sortedLinks.get(rel).add(link);
}
if (prefixingRequired && curiedLinkPresent) {
ArrayList<Object> curies = new ArrayList<Object>();
curies.add(curieProvider.getCurieInformation(new Links(links)));
sortedLinks.put("curies", curies);
}
TypeFactory typeFactory = provider.getConfig().getTypeFactory();
JavaType keyType = typeFactory.uncheckedSimpleType(String.class);
JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Object.class);
JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);
//CHANGE HERE: only thing we are changing ins the List Serializer
//shame there's not a better way to override this very specific behaviour
//without copy pasta the whole class
MapSerializer serializer = MapSerializer.construct(new String[] {}, mapType, true, null,
provider.findKeySerializer(keyType, null), new MultiLinkAwareOptionalListJackson2Serializer(property, relsAsMultilink), null);
serializer.serialize(sortedLinks, jgen, provider);
}
public MultiLinkAwareHalLinkListSerializer withForcedRels(String[] relationships) {
ImmutableSet<String> relsToForce = ImmutableSet.<String>builder().addAll(this.relsAsMultilink).add(relationships).build();
return new MultiLinkAwareHalLinkListSerializer(this.property, this.curieProvider, relsToForce);
}
#Override
public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
throws JsonMappingException {
return new MultiLinkAwareHalLinkListSerializer(property, curieProvider, this.relsAsMultilink);
}
}
public static class MultiLinkAwareOptionalListJackson2Serializer extends Jackson2HalModule.OptionalListJackson2Serializer {
private final BeanProperty property;
private final Map<Class<?>, JsonSerializer<Object>> serializers;
private final Set<String> relsAsMultilink;
public MultiLinkAwareOptionalListJackson2Serializer(BeanProperty property, Set<String> relsAsMultilink) {
super(property);
this.property = property;
this.serializers = new HashMap<Class<?>, JsonSerializer<Object>>();
this.relsAsMultilink = relsAsMultilink;
}
#Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonGenerationException {
List<?> list = (List<?>) value;
if (list.isEmpty()) {
return;
}
if(list.get(0) instanceof Link) {
Link link = (Link) list.get(0);
String rel = link.getRel();
if (list.size() > 1 || relsAsMultilink.contains(rel)) {
jgen.writeStartArray();
serializeContents(list.iterator(), jgen, provider);
jgen.writeEndArray();
} else {
serializeContents(list.iterator(), jgen, provider);
}
}
}
private void serializeContents(Iterator<?> value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonGenerationException {
while (value.hasNext()) {
Object elem = value.next();
if (elem == null) {
provider.defaultSerializeNull(jgen);
} else {
getOrLookupSerializerFor(elem.getClass(), provider).serialize(elem, jgen, provider);
}
}
}
private JsonSerializer<Object> getOrLookupSerializerFor(Class<?> type, SerializerProvider provider)
throws JsonMappingException {
JsonSerializer<Object> serializer = serializers.get(type);
if (serializer == null) {
serializer = provider.findValueSerializer(type, property);
serializers.put(type, serializer);
}
return serializer;
}
#Override
public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
throws JsonMappingException {
return new MultiLinkAwareOptionalListJackson2Serializer(property, relsAsMultilink);
}
}
public static class MultiLinkAwareHalHandlerInstantiator extends Jackson2HalModule.HalHandlerInstantiator {
private final MultiLinkAwareHalLinkListSerializer linkListSerializer;
public MultiLinkAwareHalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider) {
super(resolver, curieProvider, true);
this.linkListSerializer = new MultiLinkAwareHalLinkListSerializer(null, curieProvider, relsToForceAsAnArray);
}
#Override
public JsonSerializer<?> serializerInstance(SerializationConfig config, Annotated annotated, Class<?> serClass) {
if(serClass.equals(MultiLinkAwareHalLinkListSerializer.class)){
if (annotated.hasAnnotation(ForceMultiLink.class)) {
return this.linkListSerializer.withForcedRels(annotated.getAnnotation(ForceMultiLink.class).value());
} else {
return this.linkListSerializer;
}
} else {
return super.serializerInstance(config, annotated, serClass);
}
}
}
}
that ForceMultiLink stuff was an additional thing we ended up needing where on some resource classes a rel needed to be multi and on others it did not...so it looks like this:
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
public #interface ForceMultiLink {
String[] value();
}
you use it to annotate the getLinks() method in your resource class
I have a workaround for this issue that is along similar lines to Chris' answer. The main difference is that I did not extend Jackson2HalModule, but created a new handler-instantiator and set it as the handler-instantiator for a new instance of Jackson2HalModule that I create myself. I hope Spring HATEOAS will eventually support this functionality natively; I have a pull request that attempts to do this. Here's how I implemented my workaround:
Step 1: Create the mixin class:
public abstract class HalLinkListMixin {
#JsonProperty("_links") #JsonSerialize(using = HalLinkListSerializer.class)
public abstract List<Link> getLinks();
}
This mixin class will associate the HalLinkListSerializer (shown later) serializer with the links property.
Step 2: Create a container class that holds the rels whose link representations should always be an array of link objects:
public class HalMultipleLinkRels {
private final Set<String> rels;
public HalMultipleLinkRels(String... rels) {
this.rels = new HashSet<String>(Arrays.asList(rels));
}
public Set<String> getRels() {
return Collections.unmodifiableSet(rels);
}
}
Step 3: Create our new serializer that will override Spring HATEOAS's link-list serializer:
public class HalLinkListSerializer extends ContainerSerializer<List<Link>> implements ContextualSerializer {
private final BeanProperty property;
private CurieProvider curieProvider;
private HalMultipleLinkRels halMultipleLinkRels;
public HalLinkListSerializer() {
this(null, null, new HalMultipleLinkRels());
}
public HalLinkListSerializer(CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels) {
this(null, curieProvider, halMultipleLinkRels);
}
public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels) {
super(List.class, false);
this.property = property;
this.curieProvider = curieProvider;
this.halMultipleLinkRels = halMultipleLinkRels;
}
#Override
public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
// sort links according to their relation
Map<String, List<Object>> sortedLinks = new LinkedHashMap<>();
List<Link> links = new ArrayList<>();
boolean prefixingRequired = curieProvider != null;
boolean curiedLinkPresent = false;
for (Link link : value) {
String rel = prefixingRequired ? curieProvider.getNamespacedRelFrom(link) : link.getRel();
if (!link.getRel().equals(rel)) {
curiedLinkPresent = true;
}
if (sortedLinks.get(rel) == null) {
sortedLinks.put(rel, new ArrayList<>());
}
links.add(link);
sortedLinks.get(rel).add(link);
}
if (prefixingRequired && curiedLinkPresent) {
ArrayList<Object> curies = new ArrayList<>();
curies.add(curieProvider.getCurieInformation(new Links(links)));
sortedLinks.put("curies", curies);
}
TypeFactory typeFactory = provider.getConfig().getTypeFactory();
JavaType keyType = typeFactory.uncheckedSimpleType(String.class);
JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Object.class);
JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);
MapSerializer serializer = MapSerializer.construct(new String[]{}, mapType, true, null,
provider.findKeySerializer(keyType, null), new ListJackson2Serializer(property, halMultipleLinkRels), null);
serializer.serialize(sortedLinks, jgen, provider);
}
#Override
public JavaType getContentType() {
return null;
}
#Override
public JsonSerializer<?> getContentSerializer() {
return null;
}
#Override
public boolean hasSingleElement(List<Link> value) {
return value.size() == 1;
}
#Override
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
return null;
}
#Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
return new HalLinkListSerializer(property, curieProvider, halMultipleLinkRels);
}
private static class ListJackson2Serializer extends ContainerSerializer<Object> implements ContextualSerializer {
private final BeanProperty property;
private final Map<Class<?>, JsonSerializer<Object>> serializers = new HashMap<>();
private final HalMultipleLinkRels halMultipleLinkRels;
public ListJackson2Serializer() {
this(null, null);
}
public ListJackson2Serializer(BeanProperty property, HalMultipleLinkRels halMultipleLinkRels) {
super(List.class, false);
this.property = property;
this.halMultipleLinkRels = halMultipleLinkRels;
}
#Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
List<?> list = (List<?>) value;
if (list.isEmpty()) {
return;
}
if (list.size() == 1) {
Object element = list.get(0);
if (element instanceof Link) {
Link link = (Link) element;
if (halMultipleLinkRels.getRels().contains(link.getRel())) {
jgen.writeStartArray();
serializeContents(list.iterator(), jgen, provider);
jgen.writeEndArray();
return;
}
}
serializeContents(list.iterator(), jgen, provider);
return;
}
jgen.writeStartArray();
serializeContents(list.iterator(), jgen, provider);
jgen.writeEndArray();
}
#Override
public JavaType getContentType() {
return null;
}
#Override
public JsonSerializer<?> getContentSerializer() {
return null;
}
#Override
public boolean hasSingleElement(Object value) {
return false;
}
#Override
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
throw new UnsupportedOperationException("not implemented");
}
#Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
return new ListJackson2Serializer(property, halMultipleLinkRels);
}
private void serializeContents(Iterator<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
while (value.hasNext()) {
Object elem = value.next();
if (elem == null) {
provider.defaultSerializeNull(jgen);
} else {
getOrLookupSerializerFor(elem.getClass(), provider).serialize(elem, jgen, provider);
}
}
}
private JsonSerializer<Object> getOrLookupSerializerFor(Class<?> type, SerializerProvider provider) throws JsonMappingException {
JsonSerializer<Object> serializer = serializers.get(type);
if (serializer == null) {
serializer = provider.findValueSerializer(type, property);
serializers.put(type, serializer);
}
return serializer;
}
}
}
This class unfortunately duplicates logic, but it's not too bad. The key difference is that instead of using OptionalListJackson2Serializer, I'm using ListJackson2Serializer, which will force a rel's link representation as an array, if that rel exists in the container of rel overrides (HalMultipleLinkRels):
Step 4: Create a custom handler-instantiator:
public class HalHandlerInstantiator extends HandlerInstantiator {
private final Jackson2HalModule.HalHandlerInstantiator halHandlerInstantiator;
private final Map<Class<?>, JsonSerializer<?>> serializerMap = new HashMap<>();
public HalHandlerInstantiator(RelProvider relProvider, CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels) {
this(relProvider, curieProvider, halMultipleLinkRels, true);
}
public HalHandlerInstantiator(RelProvider relProvider, CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels, boolean enforceEmbeddedCollections) {
halHandlerInstantiator = new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider, enforceEmbeddedCollections);
serializerMap.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider, halMultipleLinkRels));
}
#Override
public JsonDeserializer<?> deserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> deserClass) {
return halHandlerInstantiator.deserializerInstance(config, annotated, deserClass);
}
#Override
public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> keyDeserClass) {
return halHandlerInstantiator.keyDeserializerInstance(config, annotated, keyDeserClass);
}
#Override
public JsonSerializer<?> serializerInstance(SerializationConfig config, Annotated annotated, Class<?> serClass) {
if(serializerMap.containsKey(serClass)) {
return serializerMap.get(serClass);
} else {
return halHandlerInstantiator.serializerInstance(config, annotated, serClass);
}
}
#Override
public TypeResolverBuilder<?> typeResolverBuilderInstance(MapperConfig<?> config, Annotated annotated, Class<?> builderClass) {
return halHandlerInstantiator.typeResolverBuilderInstance(config, annotated, builderClass);
}
#Override
public TypeIdResolver typeIdResolverInstance(MapperConfig<?> config, Annotated annotated, Class<?> resolverClass) {
return halHandlerInstantiator.typeIdResolverInstance(config, annotated, resolverClass);
}
}
This instantiator will control the lifecycle for our custom serializer. It maintains an internal instance of Jackson2HalModule.HalHandlerInstantiator, and delegates to that instance for all other serializers.
Step 5: Put it all together:
#Configuration
public class ApplicationConfiguration {
private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";
private static final String DELEGATING_REL_PROVIDER_BEAN_NAME = "_relProvider";
#Autowired
private BeanFactory beanFactory;
private static CurieProvider getCurieProvider(BeanFactory factory) {
try {
return factory.getBean(CurieProvider.class);
} catch (NoSuchBeanDefinitionException e) {
return null;
}
}
#Bean
public ObjectMapper objectMapper() {
CurieProvider curieProvider = getCurieProvider(beanFactory);
RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
//Create a new instance of Jackson2HalModule
SimpleModule module = new Jackson2HalModule();
//Provide the mix-in class so that we can override the serializer for links with our custom serializer
module.setMixInAnnotation(ResourceSupport.class, HalLinkListMixin.class);
//Register the module in the object mapper
halObjectMapper.registerModule(module);
//Set the handler instantiator on the mapper to our custom handler-instantiator
halObjectMapper.setHandlerInstantiator(new HalHandlerInstantiator(relProvider, curieProvider, halMultipleLinkRels()));
return halObjectMapper;
}
...
}
Don't forget the "self" resource link required by HAL.
In that case, that's no so common to have only one link.
I have a simple POJO that has a Map inside it.
public class Product {
public Map map;
}
then my csv looks like this:
"mapEntry1","mapEntry2","mapEntry3"
So I created a custom cell processor for parsing those:
public class MapEntryCellProcessor {
public Object execute(Object val, CsvContext context) {
return next.execute(new AbstractMap.SimpleEntry<>("somekey", val), context);
}
}
and then I add an entry setter method in my Product:
public void setName(Entry<String, String> entry) {
if (getName() == null) {
name = new HashMap<>();
}
name.put(entry.getKey(), entry.getValue());
}
Unfortunately this means I have 2 setter methods: one that accepts a map and another one that accepts an entry which doesn't really work for me (I have no control on how the POJOs are generated). Is there any other way I can parse such a CSV and have only setter that accepts a Map in my Product?
It's possible to write a cell processor that collects each column into a map. For example, the following processor allows you to specify the key and the map to add to.
package org.supercsv.example;
import java.util.Map;
import org.supercsv.cellprocessor.CellProcessorAdaptor;
import org.supercsv.cellprocessor.ift.CellProcessor;
import org.supercsv.util.CsvContext;
public class MapCollector extends CellProcessorAdaptor {
private String key;
private Map<String, String> map;
public MapCollector(String key, Map<String, String> map){
this.key = key;
this.map = map;
}
public MapCollector(String key, Map<String, String> map,
CellProcessor next){
super(next);
this.key = key;
this.map = map;
}
public Object execute(Object value, CsvContext context) {
validateInputNotNull(value, context);
map.put(key, String.valueOf(value));
return next.execute(map, context);
}
}
Then assuming your Product bean has a field name of type Map<String,String>, you can use the processor as follows.
package org.supercsv.example;
import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;
import junit.framework.TestCase;
import org.supercsv.cellprocessor.ift.CellProcessor;
import org.supercsv.io.CsvBeanReader;
import org.supercsv.io.ICsvBeanReader;
import org.supercsv.prefs.CsvPreference;
public class MapCollectorTest extends TestCase {
private static final String CSV = "John,L,Smith\n" +
"Sally,P,Jones";
public void testMapCollector() throws IOException{
ICsvBeanReader reader = new CsvBeanReader(
new StringReader(CSV),
CsvPreference.STANDARD_PREFERENCE);
// only need to map the field once, so use nulls
String[] nameMapping = new String[]{"name", null, null};
// create processors for each row (otherwise every bean
// will contain the same map!)
Product product;
while ((product = reader.read(Product.class,
nameMapping, createProcessors())) != null){
System.out.println(product.getName());
}
}
private static CellProcessor[] createProcessors() {
Map<String, String> nameMap = new HashMap<String, String>();
final CellProcessor[] processors = new CellProcessor[]{
new MapCollector("name1", nameMap),
new MapCollector("name2", nameMap),
new MapCollector("name3", nameMap)};
return processors;
}
}
This outputs:
{name3=Smith, name2=L, name1=John}
{name3=Jones, name2=P, name1=Sally}
You'll notice that while the processors execute on all 3 columns, it's only mapped to the bean once (hence the nulls in the nameMapping array).
I've also created the processors each time a row is read, otherwise every bean will be using the same map...which probably isn't what you want ;)