Multiple Java Objects to single endpoint - java

Instead of building a case statement for my Spring Boot Rest Controller, I want to have Spring use the correct endpoint. I am not even sure this is possible but I am hoping the universe could save me.
#PostMapping("/endpoint")
public String one(Greeting greet) {
return "Greeting Posted";
}
#PostMapping("/endpoint")
public String two(Address addr) {
return "Address Posted";
}
Current Error
Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'RController' method
public java.lang.String com.example.controller.RController.two(com.example.model.Address)
to {[/endpoint],methods=[POST]}: There is already 'RController' bean method
public java.lang.String com.example.controller.RController.one(com.example.model.Greeting) mapped.

This is not possible. It's ambiguous.
As a good practice, if 2 resources will handle the data differently, you must create a different endpoint for each one.
Or possible workaround for you, it's create an ViewModel object and handle it in just one method.
public class GreetingAddressVM {
private Address address;
private Greeting greeting;
}
I'd prefer creating different mapping for each action.

Related

Spring REST - Is there a way to override the character Spring uses to separate a query parameter into a list of values?

I'm writing a REST API using Spring and have certain clients to the service that cannot or will not change how they call my service.
Normally when sending a query param with a list of values you would just comma delimit the parameter and Spring will do the rest
curl http://host.com/api/endpoint?listParam=1,2,3
And the controller
#GetMapping("/api/endpoint")
public ResponseEntity endpoint(#RequestParam("listParam" List<String> listParam){
// Here, listParam is populated with 1,2,3
}
Unfortunately my clients are going to be passing lists with the bar | delimiter and it simply isn't possible to get them to change that.
Example: curl http://host.com/api/endpoint?listParam=1%7C2%7C3%7C
I would still like to use Spring to break these calls out into lists so I don't have to clutter my code with manual String.split() calls.
What I've already tried:
I found the #InitBinder annotation and wrote the following
#InitBinder
public void initBinder(WebDataBinder dataBinder){
dataBinder.registerCustomEditor(String[].class, new StringArrayPropertyEditor("|"));
}
However, this code doesn't seem to ever be called (watching with breakpoints) and requests using the bar as the delimiter fail with a 400 BAD REQUEST.
Any suggestions would be much appreciated, thanks!
404 is coming due to URL encoding issue.
You need to encode | then it will work, but it will create another problem, params would not be split.
To work around this you need to create a custom conversion that can convert String to Collection. For the custom conversion, you can check the StringToCollectionConverter class. Once you have custom conversion then you can register that service, in any of the configuration classes add following function
#Autowired
void conversionService(GenericConversionService genericConversionService) {
genericConversionService.addConverter(myStringToCollectionConvert());
}
#Bean
public MyStringToCollectionConvert myStringToCollectionConvert() {
return new MyStringToCollectionConvert();
}
In this MyStringToCollectionConvert is class that will parse String and converts to a collection of Strings.
I've accepted Sonus21's answer since his suggestion allowed me to hunt down an example that worked, but my solution was not exactly his.
The class StringToCollectionConverter did in fact exist for me, but it wasn't accessible and I couldn't use it in any way. However, in looking at the interface it implemented (ConditionalGenericConverter) and searching for more examples with Spring converters I eventually settled on the following solution.
The listParam in my question actually refers to a set of Enum values. The first thing I did was rewrite my controller to actually use the Enum values instead of raw Integers.
#GetMapping("/api/endpoint")
public ResponseEntity endpoint(#RequestParam("listParam" List<EnumClass> listParam){
// ...
}
Next, I wrote a Spring Custom Converter (Baeldung Doc)
public class CustomStringToEnumClassListConverter implements Converter<String, List<EnumClass>> {
#Override
public List<EnumClass> convert(String str) {
return Stream.of(
str.split("\\|")) // Here is where we manually delimit the incoming string with bars instead of commas
.map(i -> EnumClass.intToValue(Integer.parseInt(i))) // intToValue is a method I wrote to get the actual Enum for a given int
.collect(Collectors.toList());
}
}
Finally, I wrote a Config Bean and registered this Custom Converter with Spring:
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry){
registry.addConverter(new CustomStringToEnumClassListConverter());
}
}
Once all of this was done, Spring automatically populated the listParam list with EnumClass objects.

Proper way to create objects with Autowired fields from REST Controller [duplicate]

This question already has answers here:
Map a dto to an entity retrieved from database if Dto has Id using MapStruct
(2 answers)
Closed 2 years ago.
I'm creating some Spring app with REST Controller for communication with frontend
I have some complex objects containing reference to other objects. I want to make a Mockup class for those objects to send those mockups instead of real objects. Object -> Mockup conversion is easy, but I can't seem to find good solution for conversion of JSON objects into proper objects (I'm not receiving full data for the nested object, just some Id that let's me extract it from DB).
I think I need to #Autowire object I receive from REST POST, but I neither know if it's possible nor if it's good practice.
What's the proper solution for extracting nested dependancies for objects created from deserialized JSON?
Relevant code snippets:
public class Object {
NestedObject nestedObject;
...
}
//That's the part I'm not sure is proper solution
public class ObjectMockup {
#Autowired
private NestedObjectService nestedObjectService;
...
}
#PostMapping("/new-object")
public ObjectMockup postNewObject(#RequestBody ObjectMockup objectMockup) {
Object object = objectMockup.mockToObject();
...
return new ObjectMockup(object);
}
When I do it like that, NestedObjectService is not initialized and throws Exception as soon as I try to extract nested object from database, Probably because REST Controller did not Autowire Mockup's attribute.
Instead of having a method objectMockup.mockToObject() that fetches the data from the DB, you could:
Inject the service to the method
objectMockup.mockToObject(nestedObjectService)
Have a static factory method that takes the service and the mock:
NestedObject.mockToObject(objectMockup, nestedObjectService)
Have a separate MappingService:
#Service
public class MockToObjectMapperService {
#Autowired NestedObjectService nestedObjectService;
public Object mockToObject(objectMockup) {...}
}
// in Controller
Object object = mockToObjectMapperService.mockToObject(objectMockup);
Though your NestedObjectService could also contain this mapping method.

How to handle empty constructor with final fields

I have a class like this
public class Test {
private String m_username;
public Test() {}
public Test(String username) {
m_username = username;
}
}
And with Moxy. I can post this POJO to other API using Jersey client without any converting operation. But I need to set the m_username as a final field and that will need the empty constructor to initiate m_username. And also the Moxy doesn't work. How can I fix that?
The question isn't very well asked.
AS far as I understand:
You have to make your field final
You have to keep the empty constructor because your object is automatically serialized/deserialized in a format like JSON, using a library such as those you can find in Spring
Unfortunately, these two constraints can't be held at the same time. You will need to abandon final if you want to keep the empty constructor, and conversely.

What's the cleanest way to differientiate identical serialized objects?

I've got two root exception types my service is throwing
class ServiceException extends RuntimeException {
private Status status;
private String someString;
// Boilerplate omitted
}
class RetryableServiceException extends ServiceException {
// This class has no new fields
}
Because there's a common retry framework our clients will use which determines whether to retry or not based on the exception class.
But the problem, obviously, is that when the client gets the response and calls Response.readEntity(Class <T> entityType) they will just get an instance of whatever class they're trying to read, since they have the same fields.
Clearly I need to add some other field which distinguishes these two objects, but how can I add that to the builders and constructors in a way that:
Doesn't require a ton of client logic,
doesn't needlessly complicate the exception objects, and
can be understood by Jackson?
To answer your main issue, You don't want to couple the clients and the server so tightly by having the clients use the same exact Exception classes used on the server, create a generic error bean and map exceptions to that bean then serialise/de-serialise it. You can do that in a transparent way using javax.ws.rs.ext.ExceptionMapper, this error bean can have canRetry or shouldRetry fields. An example implementation
public class RetryableServiceExceptionMapper implements ExceptionMapper<RetryableServiceException> {
#Context
Request request;
public Response toResponse(RetryableServiceException exception) {
ApiError error = ApiError.builder().canRetry(true).message(exception.getMessage()).build();
return Response.status(status).cacheControl(cacheControl).tag(eTag).entity(error).type(APPLICATION_XML);;
}
}

Using static variables in Spring annotations

I'm using spring's PreAuthorize annotation as follows:
#PreAuthorize("hasRole('role')");
However, I already have 'role' defined as a static String on another class. If I try to use this value:
#PreAuthorize("hasRole(OtherClass.ROLE)");
I get an error:
org.springframework.expression.spel.SpelEvaluationException: EL1008E:(pos 14): Field or property 'OtherClass' cannot be found on object of type 'org.springframework.security.access.expression.method.MethodSecurityExpressionRoot'
Is there a way to access static variables like this with a PreAuthorize annotation?
Try the following which uses Spring Expression Language to evaluate the type:
#PreAuthorize("hasRole(T(fully.qualified.OtherClass).ROLE)");
Be sure to specify the fully qualified class name.
Documentation
You can also create a bean container with roles, like:
#Component("R")
public final class RoleContainer {
public static final String ROLE_A = "ROLE_A";
}
then on controller you can use:
#PreAuthorize("hasRole(#R.ROLE_A)")
To make it possible to write expressions without package names:
<sec:global-method-security>
<sec:expression-handler ref="methodSecurityExpressionHandler"/>
</sec:global-method-security>
<bean id="methodSecurityExpressionHandler" class="my.example.DefaultMethodSecurityExpressionHandler"/>
Then extend the DefaultMethodSecurityExpressionHandler:
public class DefaultMethodSecurityExpressionHandler extends org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler {
#Override
public StandardEvaluationContext createEvaluationContextInternal(final Authentication auth, final MethodInvocation mi) {
StandardEvaluationContext standardEvaluationContext = super.createEvaluationContextInternal(auth, mi);
((StandardTypeLocator) standardEvaluationContext.getTypeLocator()).registerImport("my.example");
return standardEvaluationContext;
}
}
Now create my.example.Roles.java :
public class Roles {
public static final String ROLE_UNAUTHENTICATED = "ROLE_UNAUTHENTICATED";
public static final String ROLE_AUTHENTICATED = "ROLE_AUTHENTICATED";
}
And refer to it without package name in annotations:
#PreAuthorize("hasRole(T(Roles).ROLE_AUTHENTICATED)")
instead of:
#PreAuthorize("hasRole(T(my.example.Roles).ROLE_AUTHENTICATED)")
Makes it more readable imho. Also roles are now typed. Write:
#PreAuthorize("hasRole(T(Roles).ROLE_AUTHENTICATEDDDD)")
and you will get startup errors that wouldn't have been there if you wrote:
#PreAuthorize("hasRole('ROLE_AUTHENTICATEDDDD')")
Try something like this:
#PreAuthorize("hasRole(T(com.company.enumpackage.OtherClass).ROLE.name())");
If your OtherClass enum is declared as public static, then you need to use $ sign:
#PreAuthorize("hasRole(T(com.company.ParentTopLevelClass$OtherClass).ROLE.name())");
name() to prevent futer problems if toString() will be overriden later
The accepted answer from Kevin Bowersox works, but I didn't like having the T(fully.qualified.path) stuff so I kept looking. I started by creating a custom security method using the answer from James Watkins here:
How to create custom methods for use in spring security expression language annotations
However, instead of a String, I used my enums.Permissions class as the parameter type:
#Component
public class MySecurityService {
public boolean hasPermission(enums.Permissions permission) {
...do some work here...
return true;
}
}
Now the neat part is that when I call the hasPermission from an annotation, I don't have to have to type the whole path, but I do have to enclose it in single quotes:
#PreAuthorize("#mySecurityService.hasPermission('SOME_ROLE_NAME')")
Because the hasPermission method expects an Enum, it will automatically find the Enum value with that name. If it doesn't find it you'll get an exception:
org.springframework.expression.spel.SpelEvaluationException: Type conversion problem, cannot convert from java.lang.String to enums.Permissions
You can rename hasPermission to hasRole, in which case the only trade off is that you are trading T(fully.qualified.path) for #mySecurityService and extra single quotes.
Not sure if it is any better, but there it is. Since none of this is going to verify the values at compile time anyways, my next step is to make an annotation processor.
I also have to give credit to krosenvold for pointing out that spring can automatically convert to an enum:
https://stackoverflow.com/a/516899/618881

Categories