Reputation: 415
I use Spring MVC to drive the API of an application I am currently working with. The serialization of the API response is done via Jackson's ObjectMapper
. I am faced with the following situation, we are extending a number of our objects to support UserDefinedFields (UDF) which is shown below in the abstract UserDefinedResponse
. Being a SaaS solution, multiple clients have different configuration that is stored in the database for their custom fields.
The goal of this question is to be able to respond to each client with their UDF data. This would require
customString1
, customString2
, ... to their corresponding UDF labelsExample of the abstract response
public abstract class UserDefinedResponse {
public String customString1;
public String customString2;
public String customString3;
public String customString4;
}
And response for a product that extends the UserDefinedResponse
object
public class Product extends UserDefinedResponse {
public long id;
public String name;
public float price;
}
And finally, assuming a client sets
customString1
= "supplier"
customString2
= "warehouse"
Serializing Product
for this customer should result in something similar to this:
{
"id" : 1234,
"name" : "MacBook Air",
"price" : 1299,
"supplier" : "Apple",
"warehouse" : "New York warehouse"
}
Upvotes: 2
Views: 3822
Reputation: 34460
I think you could do what you need with the help of a few Jackson annotations:
public abstract class UserDefinedResponse {
@JsonIgnore
public String customString1;
@JsonIgnore
public String customString2;
@JsonIgnore
public String customString3;
@JsonIgnore
public String customString4;
@JsonIgnore // Remove if clientId must be serialized
public String clientId;
private Map<String, Object> dynamicProperties = new HashMap<>();
@JsonAnyGetter
public Map<String, Object> getDynamicProperties() {
Mapper.fillDynamicProperties(this, this.dynamicProperties);
return this.dynamicProperties;
}
@JsonAnySetter
public void setDynamicProperty(String name, Object value) {
this.dynamicProperties.put(name, value);
Mapper.setDynamicProperty(this.dynamicProperties, name, this);
}
}
First, annotate all the properties of your base class with @JsonIgnore
, as these won't be part of the response. Then, make use of the @JsonAnyGetter
annotation to flatten the dynamicProperties
map, which will hold the dynamic properties. Finally, the @JsonAnySetter
annotation is meant to be used by Jackson on deserialization.
The missing part is the Mapper
utility class:
public abstract class Mapper<T extends UserDefinedResponse> {
private static final Map<Class<T>, Map<String, Mapper<T>>> MAPPERS = new HashMap<>();
static {
// Mappers for Products
Map<String, Mapper<Product>> productMappers = new HashMap<>();
productMappers.put("CLIENT_1", new ProductMapperClient1());
productMappers.put("CLIENT_2", new ProductMapperClient2());
// etc for rest of clients
MAPPERS.put(Product.class, productMappers);
// Mappers for Providers
Map<String, Mapper<Provider>> providerMappers = new HashMap<>();
providerMappers.put("CLIENT_1", new ProviderMapperClient1());
providerMappers.put("CLIENT_2", new ProviderMapperClient2());
// etc for rest of clients
MAPPERS.put(Provider.class, providerMappers);
// etc for rest of entities
// (each entity needs to add specific mappers for every client)
}
protected Mapper() {
}
public static void fillDynamicProperties(T response, Map<String, Object> dynamicProperties) {
// Get mapper for entity and client
Mapper<T> mapper = MAPPERS.get(response.getClass()).get(response.clientId);
// Perform entity -> map mapping
mapper.mapFromEntity(response, dynamicProperties);
}
public static void setDynamicProperty(Map<String, Object> dynamicProperties, String name, T response) {
// Get mapper for entity and client
Mapper<T> mapper = MAPPERS.get(response.getClass()).get(response.clientId);
// Perform map -> entity mapping
mapper.mapToEntity(dynamicProperties, name, response);
}
protected abstract void mapFromEntity(T response, Map<String, Object> dynamicProperties);
protected abstract void mapToEntity(Map<String, Object> dynamicProperties, String name, T response);
}
And for Product entity and client CLIENT_1:
public class ProductMapperClient1 extends Mapper<Product> {
@Override
protected void mapFromEntity(Product response, Map<String, Object> dynamicProperties) {
// Actual mapping from Product and CLIENT_1 to map
dynamicProperties.put("supplier", response.customString1);
dynamicProperties.put("warehouse", response.customString2);
}
@Override
protected void mapToEntity(Map<String, Object> dynamicProperties, String name, Product response) {
// Actual mapping from map and CLIENT_1 to Product
String property = (String) dynamicProperties.get(name);
if ("supplier".equals(name)) {
response.customString1 = property;
} else if ("warehouse".equals(name)) {
response.customString2 = property;
}
}
}
The idea is that there's a specific mapper for each (entity, client) pair. If you have many entities and/or clients, then you might consider filling the map of mappers dynamically, maybe reading from some config file and using reflection to read the properties of the entity.
Upvotes: 3
Reputation: 389
Have you considered returning Map<> as a response? Or a part of the response, like response.getUDF().get("customStringX"))? This should save you some possible trouble in the future, e.g.: 10 millions of concurrent users means 10 million classes in your VM.
Upvotes: 0