In my program, I'm getting requests from the client via Java socket. Each request has a unique command identifier which corresponds to specified command on the application side.
Now I have a class with very large switch in it, which creates instances of command classes depending on received command ID. This class receives ByteBuffer with request data from client, and ClientConnection object (a class which represents connection between client and server). It reads the first two bytes from the ByteBuffer and gets corresponding command (instance of class that extends ClientRequest class).
For example:
public static ClientRequest handle(ByteBuffer data, ClientConnection client) {
int id = data.getShort(); //here we getting command id
switch (id) {
case 1:
return new CM_ACCOUNT_LOGIN(data, client, id);
case 2:
return new CM_ENTER_GAME(data, client, id);
//...... a lot of other commands here
case 1000:
return new CM_EXIT_GAME(data, client, id);
}
//if command unknown - logging it
logUnknownRequest(client, id);
return null;
}
I don't like the large switch construction. My question is: Is there some ways to refactor this code to make it more elegant? Maybe use some pattern?
Also, in future I want to try to use dependency injection (Guice) in my program, could it be used for instantiating ClientRequest instances depending on received ID?
Mapping an ID to a response object is a common task, but it is difficult to move away from somehow enumerating which ID maps to a specific response object. The switch block you have provided works, but it is not the most extensible. For example, if a new response object or ID is added, you would have to add a case statement to the switch.
One alternative is to create a map of IDs to a factory object that can create new response objects. For example:
#FunctionalInterface
public interface ClientRequestFactory {
public ClientRequest createClientRequest(ByteBuffer data, ClientConnection client, int id);
}
public class ClientRequestSwitchboard {
private final Map<Integer, ClientRequestFactory> mappings = new HashMap<>();
public ClientRequestSwitchboard() {
mappings.put(1, (data, client, id) -> new CM_ACCOUNT_LOGIN(data, client, id));
mappings.put(2, (data, client, id) -> new CM_ENTER_GAME(data, client, id));
// ... Add each of the remaining request types ...
}
public ClientRequest createClientRequest(ByteBuffer data, ClientConnection client, int id) {
ClientRequestFactory factory = mappings.get(id);
if (factory == null) {
return createDefault(data, client, id);
}
else {
return factory.createClientRequest(data, client, id);
}
}
protected ClientRequest createDefault(ByteBuffer data, ClientConnection client, int id) {
logUnknownRequest(client, id);
return null;
}
}
You can then use the ClientRequestSwitchboard as follows:
private static final ClientRequestSwitchboard switchboard = new ClientRequestSwitchboard();
public static ClientRequest handle(ByteBuffer data, ClientConnection client) {
int id = data.getShort();
return switchboard.createClientRequest(data, client, id);
}
The benefit of this approach over the switch technique is that you now store the mapping information as dynamic data rather than as static case statements. In the dynamic-approach, we can add or remove mappings at runtime, rather than only at compile-time (by adding a new case statement). Although this may appear to be a slight difference, the dynamic-approach allows us to improve the solution much further.
If we employ a Dependency Injection (DI) framework, such as Spring, we can utilize some creative features in Java. For example, we can add new ClientRequestFactory instances (new entries in the map) by creating a new ClientRequestFactory classes. For example:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
public #interface ClientRequestFactoryForId {
public int value();
}
#Service
#ClientRequestFactoryForId(1)
public class AccountLoginClientRequestFactory implements ClientRequestFactory {
#Override
public ClientRequest createClientRequest(ByteBuffer data, ClientConnection client, int id) {
new CM_ACCOUNT_LOGIN(data, client, id);
}
}
#Service
public class ClientRequestSwitchboard {
private final Map<Integer, ClientRequestFactory> mappings = new HashMap<>();
private final ListableBeanFactory beanFactory;
#Autowired
public ClientRequestSwitchboard(ListableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
#PostConstruct
#SuppressWarnings("unchecked")
private void findAllClientRequestFactories() {
Map<String, Object> factories = beanFactory.getBeansWithAnnotation(ClientRequestFactoryForId.class);
for (Object factory: factories.values()) {
int id = dataStore.getClass().getAnnotation(ClientRequestFactoryForId.class).value();
if (factory instanceof ClientRequestFactory) {
mappings.put(id, (ClientRequestFactory) factory);
}
else {
throw new IllegalStateException("Found object annotated as #ClientRequestFactoryForId but was not a ClientRequestFactory instance: " + factory.getClass().getName());
}
}
}
public ClientRequest createClientRequest(ByteBuffer data, ClientConnection client, int id) {
ClientRequestFactory factory = mappings.get(id);
if (factory == null) {
return createDefault(data, client, id);
}
else {
return request.createClientRequest(data, client, id);
}
}
protected ClientRequest createDefault(ByteBuffer data, ClientConnection client, int id) {
logUnknownRequest(client, id);
return null;
}
}
This technique uses Spring to find all classes with a specific annotation (in this case, ClientRequestFactoryForId) and register each as a factory that can create ClientRequest objects. A type-safety check is performed, since we do not know if an object annotated with ClientRequestFactoryForId actually implements ClientRequestFactory, even though we expect it to. To add a new factory, we simply create a new bean with the ClientRequestFactoryForId annotation:
#Service
#ClientRequestFactoryForId(2)
public class AccountLoginClientRequestFactory implements ClientRequestFactory {
#Override
public ClientRequest createClientRequest(ByteBuffer data, ClientConnection client, int id) {
new CM_ENTER_GAME(data, client, id);
}
}
This solution assumes that the ClientRequestSwitchboard and each of the classes annotated with ClientRequestFactoryForId are beans that are known to the Spring application context (are annotated with Component or some other derivative of Component, such as Service, and the directory in which these beans exist are picked up by a component scan or are explicitly created in a #Configuration class). For more information, see the Spring Framework Guru's article on Component Scanning.
Summary
At some level, the ID to ClientRequest mapping must be established
Establishing the mapping at runtime opens up many more options
Spring can be used to decouple the dependency between factory beans that create ClientRequest objects and the ClientRequestSwitchboard
Related
I have a REST service, which contains three classes in one module(bundle)
User.java -> Entity
UserService.java -> REST service
UserValidation.java -> Special validator for the entity. Server send entity to this validator and get validation result (true or false):
User.java
#XmlRootElement(name = "User")
public class User {
private long id;
private String name;
private String surname;
private String patronymic;
/*Getters and Setters*/
}
UserService.java
public class UserServiceImpl implements UserService {
private UserDAO userDbDao = new UserDatabaseDAO();
#POST
#Path("/users/")
public Response addUser(User user) {
UserValidator userValidator = new UserValidator(user);
if (userValidator.isValid()) {
User newUser = userDbDao.createUser(user);
return Response.ok().type("application/xml").entity(newUser).build();
} else {
return Response.status(Response.Status.BAD_REQUEST).entity(userValidator.getErrorMessage()).build();
}
}
}
UserValidator.java
public class UserValidator {
private static final int MAX_SIZE_NAME = 50;
private static final int MIN_SIZE_NAME = 2;
private User user;
public UserValidator(User user) {
this.user = user;
}
private BadUserResponse badUserResponse = new BadUserResponse();
private boolean isNameValid(String name) {
if (name == null) {
badUserResponse.setNsp("Null in fields name/surname/patronymic");
return false;
}
String tempName = name.trim();
if (tempName.length() < MIN_SIZE_NAME || tempName.length() > MAX_SIZE_NAME) {
badUserResponse.setNsp(String.format("Fields name/surname/patronymic too long or too short (Allowed length from %d to %d)", MIN_SIZE_NAME, MAX_SIZE_NAME));
return false;
}
for (int i = 0; i < tempName.length(); i++) {
if (!Character.isLetter(tempName.charAt(i))) {
badUserResponse.setNsp("Fields name/surname/patronymic contains wrong symbols (Only letters allowed)");
return false;
}
}
return true;
}
public boolean isValid() {
return (isNameValid(user.getName()) &
isNameValid(user.getSurname()) &
isNameValid(user.getPatronymic()));
}
public BadUserResponse getErrorMessage() {
return badUserResponse;
}
BadUserResponse.java
#XmlRootElement(name="baduserresponce")
public class BadUserResponse {
private String nsp;
public String getNsp() {
return nsp;
}
public void setNsp(String nsp) {
this.nsp = nsp;
}
}
But now, I need to split this into separate bundles. Because, as you can see, they uses functionality of each other. For example UserService.java
just used this UserValidator userValidator = new UserValidator(user);
I need to connect these bundles somehow (OSGI Service, ActiveMQ).
In my opinion it works something like this:
UserService bundle get User entity from REST method.
Put all of the User fields (name, surname, patronymic) to ActiveMQ queue (because UserValidator bundle don't know what's is User entity).
UserValidator bundle get User's fiels from queue and validate them.
UserValidator bundle put validation result (true/false) to queue.
UserService bundle get validation result from queue and send User to DAO.
But this is just a concept. Am I wrong?
What's the best way to pass entity though bundles and how should I do this?
You current way of simply initiating the UserValidator via new is technically fine even if they live in different bundles. If your validator is only needed in this place and is simple I would even leave it in the same bundle.
The other options can make sense to decouple your bundles. Using messaging allows you to avoid sync calls. It can also be use to send the data to a remote machine. JMS messaging is quite heavy weight though. You need a broker and depend on the API. In your case you also directly need the result of the validation. So you would simulate a sync call with JMS. So I would rather avoid this.
Using an OSGi service allows you to decouple from the implementation of the service. In this case it makes sense to create an interface for UserValidator. I would also put this interface into a separate bundle. You then need to register the service in the bundle that implements the validator and bind the service in the bundle that uses the validator. OSGi services are very light weight and by default synchronous. So I think they would fit your problem well.
For registering and binding services do not use the OSGi API directly. Instead use declarative services with annotations. They take away most of the complexity in dealing with OSGi services.
Btw. I am not sure how you do REST. I suggest to have a look at the Aries JAX-RS Whiteboard.
I have a Dynamo DB DAO class, which takes the region like us-east-1, us-east-2 etc., to instantiate the object and interacts with DDB.
Now I am processing a stream of messages, which contains this region value along with other payload to be written to DDB. I want to ensure a single instance of DAO object is created per region.
Currently I have created a map holding all the Dao instances per region and using it for each request to achieve this.
Sample code that I'm using looks like below.
public class DDBDao {
private DynamoDBMapper dynamoDBMapper;
public DDBDao(final string region) {
AmazonDynamoDB dynamoDBClient = AmazonDynamoDBClientBuilder.standard()
.withRegion(Regions.fromName(region))
.build();
this.dynamoDBMapper = new DynamoDBMapper(dynamoDBClient);
}
public save(..) {
dynamoDBMapper.save(...)
}
....
}
#Singleton
public class DaoContainer {
Map<String, DDBDao> daoContainer = new HashMap<>();
DaoContainer() {
daoContainer.put("us-east-1", new DDBDao("us-east-1"));
daoContainer.put("us-east-2", new DDBDao("us-east-2"));
.....
}
}
I create a instance of DaoContainer and get the DDBDao for the given region to interact with DynamoDB.
What is the best way to create singleton instances of DDBDao per region?
I would suggest implementing custom Region scoped bean, this works exactly same as request/ session scope beans except spring will maintain bean object per Region.
org.springframework.beans.factory.config.Scope is an interface and by implementing it one can create a custom scope in the spring container
public class RegionScope implements Scope {
private final ThreadLocal regionScope = new ThreadLocal() {
protected Object initialValue() {
return new HashMap();
}
};
public Object get(String name, ObjectFactory objectFactory) {
Map scope = (Map) regionScope.get();
Object object = scope.get(name);
if (object == null) {
object = objectFactory.getObject();
scope.put(name, object);
}
return object;
}
public Object remove(String name) {
Map scope = (Map) regionScope.get();
return scope.remove(name);
}
}
I'm trying to implement a partial update of the Manager entity based in the following:
Entity
public class Manager {
private int id;
private String firstname;
private String lastname;
private String username;
private String password;
// getters and setters omitted
}
SaveManager method in Controller
#RequestMapping(value = "/save", method = RequestMethod.PATCH)
public #ResponseBody void saveManager(#RequestBody Manager manager){
managerService.saveManager(manager);
}
Save object manager in Dao impl.
#Override
public void saveManager(Manager manager) {
sessionFactory.getCurrentSession().saveOrUpdate(manager);
}
When I save the object the username and password has changed correctly but the others values are empty.
So what I need to do is update the username and password and keep all the remaining data.
If you are truly using a PATCH, then you should use RequestMethod.PATCH, not RequestMethod.POST.
Your patch mapping should contain the id with which you can retrieve the Manager object to be patched. Also, it should only include the fields with which you want to change. In your example you are sending the entire entity, so you can't discern the fields that are actually changing (does empty mean leave this field alone or actually change its value to empty).
Perhaps an implementation as such is what you're after?
#RequestMapping(value = "/manager/{id}", method = RequestMethod.PATCH)
public #ResponseBody void saveManager(#PathVariable Long id, #RequestBody Map<Object, Object> fields) {
Manager manager = someServiceToLoadManager(id);
// Map key is field name, v is value
fields.forEach((k, v) -> {
// use reflection to get field k on manager and set it to value v
Field field = ReflectionUtils.findField(Manager.class, k);
field.setAccessible(true);
ReflectionUtils.setField(field, manager, v);
});
managerService.saveManager(manager);
}
Update
I want to provide an update to this post as there is now a project that simplifies the patching process.
The artifact is
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId>
<version>1.13</version>
</dependency>
The implementation to patch the Manager object in the OP would look like this:
Controller
#Operation(summary = "Patch a Manager")
#PatchMapping("/{managerId}")
public Task patchManager(#PathVariable Long managerId, #RequestBody JsonPatch jsonPatch)
throws JsonPatchException, JsonProcessingException {
return managerService.patch(managerId, jsonPatch);
}
Service
public Manager patch(Long managerId, JsonPatch jsonPatch) throws JsonPatchException, JsonProcessingException {
Manager manager = managerRepository.findById(managerId).orElseThrow(EntityNotFoundException::new);
JsonNode patched = jsonPatch.apply(objectMapper.convertValue(manager, JsonNode.class));
return managerRepository.save(objectMapper.treeToValue(patched, Manager.class));
}
The patch request follows the specifications in RFC 6092, so this is a true PATCH implementation. Details can be found here
With this, you can patch your changes
1. Autowire `ObjectMapper` in controller;
2. #PatchMapping("/manager/{id}")
ResponseEntity<?> saveManager(#RequestBody Map<String, String> manager) {
Manager toBePatchedManager = objectMapper.convertValue(manager, Manager.class);
managerService.patch(toBePatchedManager);
}
3. Create new method `patch` in `ManagerService`
4. Autowire `NullAwareBeanUtilsBean` in `ManagerService`
5. public void patch(Manager toBePatched) {
Optional<Manager> optionalManager = managerRepository.findOne(toBePatched.getId());
if (optionalManager.isPresent()) {
Manager fromDb = optionalManager.get();
// bean utils will copy non null values from toBePatched to fromDb manager.
beanUtils.copyProperties(fromDb, toBePatched);
updateManager(fromDb);
}
}
You will have to extend BeanUtilsBean to implement copying of non null values behaviour.
public class NullAwareBeanUtilsBean extends BeanUtilsBean {
#Override
public void copyProperty(Object dest, String name, Object value)
throws IllegalAccessException, InvocationTargetException {
if (value == null)
return;
super.copyProperty(dest, name, value);
}
}
and finally, mark NullAwareBeanUtilsBean as #Component
or
register NullAwareBeanUtilsBean as bean
#Bean
public NullAwareBeanUtilsBean nullAwareBeanUtilsBean() {
return new NullAwareBeanUtilsBean();
}
First, you need to know if you are doing an insert or an update. Insert is straightforward. On update, use get() to retrieve the entity. Then update whatever fields. At the end of the transaction, Hibernate will flush the changes and commit.
You can write custom update query which updates only particular fields:
#Override
public void saveManager(Manager manager) {
Query query = sessionFactory.getCurrentSession().createQuery("update Manager set username = :username, password = :password where id = :id");
query.setParameter("username", manager.getUsername());
query.setParameter("password", manager.getPassword());
query.setParameter("id", manager.getId());
query.executeUpdate();
}
ObjectMapper.updateValue provides all you need to partially map your entity with values from dto.
As an addition, you can use either of two here: Map<String, Object> fields or String json, so your service method may look like this:
#Autowired
private ObjectMapper objectMapper;
#Override
#Transactional
public Foo save(long id, Map<String, Object> fields) throws JsonMappingException {
Foo foo = fooRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Foo not found for this id: " + id));
return objectMapper.updateValue(foo , fields);
}
As a second solution and addition to Lane Maxwell's answer you could use Reflection to map only properties that exist in a Map of values that was sent, so your service method may look like this:
#Override
#Transactional
public Foo save(long id, Map<String, Object> fields) {
Foo foo = fooRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Foo not found for this id: " + id));
fields.keySet()
.forEach(k -> {
Method method = ReflectionUtils.findMethod(LocationProduct.class, "set" + StringUtils.capitalize(k));
if (method != null) {
ReflectionUtils.invokeMethod(method, foo, fields.get(k));
}
});
return foo;
}
Second solution allows you to insert some additional business logic into mapping process, might be conversions or calculations ect.
Also unlike finding reflection field Field field = ReflectionUtils.findField(Foo.class, k); by name and than making it accessible, finding property's setter actually calls setter method that might contain additional logic to be executed and prevents from setting value to private properties.
I have a REST API specification that talks with back-end microservices, which return the following values:
On "collections" responses (e.g. GET /users) :
{
users: [
{
... // single user object data
}
],
links: [
{
... // single HATEOAS link object
}
]
}
On "single object" responses (e.g. GET /users/{userUuid}) :
{
user: {
... // {userUuid} user object}
}
}
This approach was chosen so that single responses would be extensible (for example, maybe if GET /users/{userUuid} gets an additional query parameter down the line such at ?detailedView=true we would have additional request information).
Fundamentally, I think it is an OK approach for minimizing breaking changes between API updates. However, translating this model to code is proving very arduous.
Let's say that for single responses, I have the following API model object for a single user:
public class SingleUserResource {
private MicroserviceUserModel user;
public SingleUserResource(MicroserviceUserModel user) {
this.user = user;
}
public String getName() {
return user.getName();
}
// other getters for fields we wish to expose
}
The advantage of this method is that we can expose only the fields from the internally used models for which we have public getters, but not others. Then, for collections responses I would have the following wrapper class:
public class UsersResource extends ResourceSupport {
#JsonProperty("users")
public final List<SingleUserResource> users;
public UsersResource(List<MicroserviceUserModel> users) {
// add each user as a SingleUserResource
}
}
For single object responses, we would have the following:
public class UserResource {
#JsonProperty("user")
public final SingleUserResource user;
public UserResource(SingleUserResource user) {
this.user = user;
}
}
This yields JSON responses which are formatted as per the API specification at the top of this post. The upside of this approach is that we only expose those fields that we want to expose. The heavy downside is that I have a ton of wrapper classes flying around that perform no discernible logical task aside from being read by Jackson to yield a correctly formatted response.
My questions are the following:
How can I possibly generalize this approach? Ideally, I would like to have a single BaseSingularResponse class (and maybe a BaseCollectionsResponse extends ResourceSupport class) that all my models can extend, but seeing how Jackson seems to derive the JSON keys from the object definitions, I would have to user something like Javaassist to add fields to the base response classes at Runtime - a dirty hack that I would like to stay as far away from as humanly possible.
Is there an easier way to accomplish this? Unfortunately, I may have a variable number of top-level JSON objects in the response a year from now, so I cannot use something like Jackson's SerializationConfig.Feature.WRAP_ROOT_VALUE because that wraps everything into a single root-level object (as far as I am aware).
Is there perhaps something like #JsonProperty for class-level (as opposed to just method and field level)?
There are several possibilities.
You can use a java.util.Map:
List<UserResource> userResources = new ArrayList<>();
userResources.add(new UserResource("John"));
userResources.add(new UserResource("Jane"));
userResources.add(new UserResource("Martin"));
Map<String, List<UserResource>> usersMap = new HashMap<String, List<UserResource>>();
usersMap.put("users", userResources);
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(usersMap));
You can use ObjectWriter to wrap the response that you can use like below:
ObjectMapper mapper = new ObjectMapper();
ObjectWriter writer = mapper.writer().withRootName(root);
result = writer.writeValueAsString(object);
Here is a proposition for generalizing this serialization.
A class to handle simple object:
public abstract class BaseSingularResponse {
private String root;
protected BaseSingularResponse(String rootName) {
this.root = rootName;
}
public String serialize() {
ObjectMapper mapper = new ObjectMapper();
ObjectWriter writer = mapper.writer().withRootName(root);
String result = null;
try {
result = writer.writeValueAsString(this);
} catch (JsonProcessingException e) {
result = e.getMessage();
}
return result;
}
}
A class to handle collection:
public abstract class BaseCollectionsResponse<T extends Collection<?>> {
private String root;
private T collection;
protected BaseCollectionsResponse(String rootName, T aCollection) {
this.root = rootName;
this.collection = aCollection;
}
public T getCollection() {
return collection;
}
public String serialize() {
ObjectMapper mapper = new ObjectMapper();
ObjectWriter writer = mapper.writer().withRootName(root);
String result = null;
try {
result = writer.writeValueAsString(collection);
} catch (JsonProcessingException e) {
result = e.getMessage();
}
return result;
}
}
And a sample application:
public class Main {
private static class UsersResource extends BaseCollectionsResponse<ArrayList<UserResource>> {
public UsersResource() {
super("users", new ArrayList<UserResource>());
}
}
private static class UserResource extends BaseSingularResponse {
private String name;
private String id = UUID.randomUUID().toString();
public UserResource(String userName) {
super("user");
this.name = userName;
}
public String getUserName() {
return this.name;
}
public String getUserId() {
return this.id;
}
}
public static void main(String[] args) throws JsonProcessingException {
UsersResource userCollection = new UsersResource();
UserResource user1 = new UserResource("John");
UserResource user2 = new UserResource("Jane");
UserResource user3 = new UserResource("Martin");
System.out.println(user1.serialize());
userCollection.getCollection().add(user1);
userCollection.getCollection().add(user2);
userCollection.getCollection().add(user3);
System.out.println(userCollection.serialize());
}
}
You can also use the Jackson annotation #JsonTypeInfo in a class level
#JsonTypeInfo(include=As.WRAPPER_OBJECT, use=JsonTypeInfo.Id.NAME)
Personally I don't mind the additional Dto classes, you only need to create them once, and there is little to no maintenance cost. And If you need to do MockMVC tests, you will most likely need the classes to deserialize your JSON responses to verify the results.
As you probably know the Spring framework handles the serialization/deserialization of objects in the HttpMessageConverter Layer, so that is the correct place to change how objects are serialized.
If you don't need to deserialize the responses, it is possible to create a generic wrapper, and a custom HttpMessageConverter (and place it before MappingJackson2HttpMessageConverter in the message converter list). Like this:
public class JSONWrapper {
public final String name;
public final Object object;
public JSONWrapper(String name, Object object) {
this.name = name;
this.object = object;
}
}
public class JSONWrapperHttpMessageConverter extends MappingJackson2HttpMessageConverter {
#Override
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
// cast is safe because this is only called when supports return true.
JSONWrapper wrapper = (JSONWrapper) object;
Map<String, Object> map = new HashMap<>();
map.put(wrapper.name, wrapper.object);
super.writeInternal(map, type, outputMessage);
}
#Override
protected boolean supports(Class<?> clazz) {
return clazz.equals(JSONWrapper.class);
}
}
You then need to register the custom HttpMessageConverter in the spring configuration which extends WebMvcConfigurerAdapter by overriding configureMessageConverters(). Be aware that doing this disables the default auto detection of converters, so you will probably have to add the default yourself (check the Spring source code for WebMvcConfigurationSupport#addDefaultHttpMessageConverters() to see defaults. if you extend WebMvcConfigurationSupport instead WebMvcConfigurerAdapter you can call addDefaultHttpMessageConverters directly (Personally I prefere using WebMvcConfigurationSupport over WebMvcConfigurerAdapter if I need to customize anything, but there are some minor implications to doing this, which you can probably read about in other articles.
Jackson doesn't have a lot of support for dynamic/variable JSON structures, so any solution that accomplishes something like this is going to be pretty hacky as you mentioned. As far as I know and from what I've seen, the standard and most common method is using wrapper classes like you are currently. The wrapper classes do add up, but if you get creative with your inheretence you may be able to find some commonalities between classes and thus reduce the amount of wrapper classes. Otherwise you might be looking at writing a custom framework.
I guess you are looking for Custom Jackson Serializer. With simple code implementation same object can be serialized in different structures
some example:
https://stackoverflow.com/a/10835504/814304
http://www.davismol.net/2015/05/18/jackson-create-and-register-a-custom-json-serializer-with-stdserializer-and-simplemodule-classes/
For example, I have a room
public class Room {
private int id;
private Set<User> users;
}
So I want it to be endpoint for my websocket application. But there may be a lot of rooms and I want each of them could have own URI (for example, rooms/1, rooms/2 etc.)
Evidently, #ServerEnpoint annotaion allows only constants. So, is there any way to make it?
Something like this:
#ServerEndpoint(value = "/rooms/{roomnumber}")
public class....
static Map<String, Session> openSessions = ...
#OnOpen
public void onConnectionOpen(final Session session, #PathParam("roomnumber") final String roomnumber,
...
//store roomnumber in session
session.getUserProperties().put("roomnumber", roomnumber);
openSessions.put( String.valueOf(session.getId()), session )
To only send messages to specific roomnumbers/clients:
// check if session corresponds to the roomnumber
for (Map.Entry<String, Session> entry : openSessions.entrySet()) {
Session s = entry.getValue();
if (s.isOpen() && s.getUserProperties().get("roomnumber").equals(roomnumber_you_want_to_address)) {
...
And when a client disconnects:
#OnClose
public void onConnectionClose(Session session) {
openSessions.remove(session.getId());
}
You can use this per function to map requests with different variables in the same controller
#RequestMapping(value = "/endpoint/{endpointVariable}", method = RequestMethod.GET)
public ReturnDTO getReturnDTO(<params>){
// Here the variable, endpointVariable, will be accessible
// In my experiences its always been an integer, but I'm sure a string
// would be possible. check with debugger
}
http://www.journaldev.com/3358/spring-mvc-requestmapping-annotation-example-with-controller-methods-headers-params-requestparam-pathvariable