In Spring, is it possible to use #JsonView on URL query parameter objects? I can do it for #RequestBody but we don't have bodies in GET requests. This question is specifically for URL query parameters that have been converted to objects by Spring.
For example, I want to have a controller with this mapping:
#GetMapping("/user")
ResponseEntity<UserDTO> searchUser(#JsonView(value = UserView.Searchable.class) UserDTO userQuery) {
//Do some work here using userQuery object for searching users
return ResponseEntity.ok();
}
UserDTO:
public class UserDTO {
#JsonProperty("id")
#JsonView(UserView.Private.class)
private String id= null;
#JsonView(UserView.Searchable.class)
#JsonProperty("city")
private String city = null;
#JsonProperty("country")
#JsonView(UserView.Searchable.class)
private String country = null;
#JsonProperty("state")
private String state = null;
#JsonProperty("zipCode")
private String zipCode = null;
//More properties and getter/setters...etc
}
So if I wanted to call the endpoint I could create a URL like
localhost:8080//api/user?country=Canada to search for a user in Canada but if I tried localhost:8080//api/user?id=123, the property would be ignored.
EDIT:
I might have rushed this idea. There is no JSON de-serialization from url parameters because they are not JSON. Spring creates the query object from ServletModelAttributeMethodProcessor. Perhaps if I want some custom behavior I need to implement HandlerMethodArgumentResolver and do it myself.
EDIT 2
I'm a bit new to Spring so I have a lot to learn but I think what Ill do is just use #InitBinder to whitelist the fields for binding
#InitBinder
public void setSearchableFields(WebDataBinder binder) {
binder.setAllowedFields(
"city",
"country"
);
}
In one of my projects, I have used a POJO just for the query params. I doubt any property would be ignored by default in spring, you can have null checks to ignore.
QueryParams.java
#Data
public class QueryParams {
Integer page;
Integer pageSize;
String sortBy;
Sort.Direction direction;
String searchId;
String status;
String symbol;
public PageRequest getPageRequest(){
if(this.page==null){
this.page = 0;
}
if(this.pageSize==null){
this.pageSize = 25;
}
if(this.sortBy==null){
this.sortBy = "createdAt";
}
if(this.direction ==null){
this.direction = Sort.Direction.DESC;
}
return PageRequest.of(this.page, this.pageSize, Sort.by(this.direction, this.sortBy));
}
}
And my controller :
#GetMapping("currencies")
public ResponseEntity<Page<CurrencyConfig>> getAllCurrencies(#Valid QueryParams queryParams) {
try {
return ResponseEntity.ok(orderbookService.getAllCurrencyConfig(queryParams));
} catch (HttpClientErrorException | HttpServerErrorException e) {
throw new ResponseStatusException(e.getStatusCode(), e.getMessage());
} catch (Exception e) {
e.printStackTrace();
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
e.getLocalizedMessage());
}
}
Related
We are developing a project Using Spring boot with DGS Netflix graphql. We are created all the schemas and datafethers which is working absolutely fine with a default endpoint "/graphql". we would like to expose this app with custom endpoing so we are trying to add a controller with a custom endpoint as below. But When i run the application and send a query, my data fetcher is called twice . first time called from controller and second time i believe from framework it self. Anybody has any thoughts on this why its being called twice and how to avoid it? You help is highly appreciated. Please see the below Datafetcher and Controller.
Controller:
#RestController
#RequestMapping("/sample-information/model")
#Slf4j
public class CustomController {
#Autowired
DgsQueryExecutor dgsQueryExecutor;
#PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, "application/graphql"})
public Mono<ResponseEntity<Object>> getDetails(#RequestBody String query,
#RequestHeader HttpHeaders headers
) {
GraphQLQueryInput inputs = null;
try {
inputs = ObjectMapperHelper.objectMapper.readValue(query, GraphQLQueryInput.class);
} catch (Exception e) {
log.error("TraceId: {} - Application Error: Error message: Error converting query to GraphQLQueryInput: {} "+ query);
}
if(inputs.getVariables() == null) {
inputs.setVariables(new HashMap<>());
}
if(inputs.getOperationName() == null) {
inputs.setOperationName("");
}
final String que = inputs.getQuery();
final Map<String, Object> var = inputs.getVariables();
final String opn = inputs.getOperationName();
ExecutionInput.Builder executionInput = ExecutionInput.newExecutionInput()
.query(inputs.getQuery())
.operationName(inputs.getOperationName())
.variables(inputs.getVariables());
return Mono.fromCallable(()-> {
return dgsQueryExecutor.execute(que, var, opn);
}).subscribeOn(Schedulers.elastic()).map(result -> {
return new ResponseEntity<>(result, HttpStatus.OK);
});
}
}
Datafetcher:
#DgsComponent
#Slf4j
public class SampleDataFetcher {
#Autowired
SampleBuilder sampleBuilder;
#DgsData(parentType = DgsConstants.QUERY_TYPE, field = DgsConstants.QUERY.SampleField)
public CompletableFuture<StoreDirectoryByStateResponse> getStoreDirectoryByState(#InputArgument String state, DataFetchingEnvironment dfe) throws BadRequestException {
Mono<StoreDirectoryByStateResponse> storeDirectoryResponse = null;
try {
sampleResponse = sampleBuilder.buildResponse(modelGraphQLContext);
} catch (BaseException e) {
}
return sampleResponse.map(response -> {
return response;
}).toFuture();
}
}
I want to return all values of enum using graphql.
I have schema:
schema {
query: Query
}
type Query {
getDataTypes: [DictionaryType]
}
enum DictionaryType{
RISK
SALES_CHANNEL
PERSON_TYPE
}
We have normal java enum:
public enum DictionaryType {
RISK,
SALES_CHANNEL,
PERSON_TYPE
}
and Controller with configuration:
public class DictionaryController {
#Value("classpath:items.graphqls")
private Resource schemaResource;
private GraphQL graphQL;
private final DictionaryService dictionaryService;
#PostConstruct
public void loadSchema() throws IOException {
File schemaFile = schemaResource.getFile();
TypeDefinitionRegistry registry = new SchemaParser().parse(schemaFile);
RuntimeWiring wiring = buildWiring();
GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(registry, wiring);
graphQL = GraphQL.newGraphQL(schema).build();
}
private RuntimeWiring buildWiring() {
DataFetcher<Set<DictionaryType>> fetcher3 = dataFetchingEnvironment -> {
return dictionaryService.getDictionaryTypes();
};
return RuntimeWiring.newRuntimeWiring().type("Query", typeWriting ->
typeWriting
.dataFetcher("getDataTypes", fetcher3))
.build();
}
#PostMapping("getDataTypes")
public ResponseEntity<Object> getDataTypes(#RequestBody String query) {
ExecutionResult result = graphQL.execute(query);
return new ResponseEntity<Object>(result, HttpStatus.OK);
}
}
When i make POST to http://localhost:50238/getDataTypes
with body:
{
getDataTypes {
}
}
I get "errorType": "InvalidSyntax",
in response.
That's an invalid query as you have braces with no content (i.e. { }). Your schema suggests that the query should be much simpler though:
{ getDataTypes }
I've already have a look at the question "Jackson dynamic property names" but it does not really answer to my question.
I want to deserialize something like this :
public class Response<T> {
private String status;
private Error error;
private T data;
}
but data can have different names since different services exist and return the same structure with some different data. For example 'user' and 'contract' :
{
response: {
status: "success",
user: {
...
}
}
}
or
{
response: {
status: "failure",
error : {
code : 212,
message : "Unable to retrieve contract"
}
contract: {
...
}
}
}
I'd like genericize my responses objects like this :
public class UserResponse extends Response<User> {}
I've tried the following but i'm not sure it is my use case or if don't use it in the good way :
#JsonTypeInfo(include = As.WRAPPER_OBJECT, use = Id.CLASS)
#JsonSubTypes({#Type(value = User.class, name = "user"),
#Type(value = Contract.class, name = "contract")})
Finally, i've created a custom Deserializer. It works but i'm not satisfied:
public class ResponseDeserializer extends JsonDeserializer<Response> {
#Override
public Response deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
ObjectMapper mapper = new ObjectMapper();
Response responseData = new Response();
Object data = null;
for (; jp.getCurrentToken() != JsonToken.END_OBJECT; jp.nextToken()) {
String propName = jp.getCurrentName();
// Skip field name:
jp.nextToken();
if ("contract".equals(propName)) {
data = mapper.readValue(jp, Contract.class);
} else if ("user".equals(propName)) {
data = mapper.readValue(jp, User.class);
} else if ("status".equals(propName)) {
responseData.setStatus(jp.getText());
} else if ("error".equals(propName)) {
responseData.setError(mapper.readValue(jp, com.ingdirect.dg.business.object.community.api.common.Error.class));
}
}
if (data instanceof Contract) {
Response<Contract> response = new Response<Ranking>(responseData);
return response;
}
if (data instanceof User) {
Response<User> response = new Response<User>(responseData);
return response;
}
// in all other cases, the type is not yet managed, add it when needed
throw new JsonParseException("Cannot parse this Response", jp.getCurrentLocation());
}
}
Any idea to do this clean with annotations ? Thanks in advance !
Jackson framework provides inbuilt support for dynamic types.
//Base type
#JsonTypeInfo(property = "type", use = Id.NAME)
#JsonSubTypes({ #Type(ValidResponse.class),
#Type(InvalidResponse.class)
})
public abstract class Response<T> {
}
//Concrete type 1
public class ValidResponse extends Response<T>{
}
//Concrete type 2
public class InvalidResponse extends Response<T>{
}
main {
ObjectMapper mapper = new ObjectMapper();
//Now serialize
ValidResponse response = (ValidResponse)(mapper.readValue(jsonString, Response.class));
//Deserialize
String jsonString = mapper.writeValueAsString(response);
}
Have you tried:
public class AnyResponse {
private String status;
private Error error;
private Contract contract;
private User user;
// And all other possibilities.
}
// ...
mapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
This should fill in whatever object appears in the JSON and leave the rest null.
You could then fill in a Response with the relevant object.
I am using Spring MVC and returning JSON as response. I would like to create a generic JSON response where I can put in any TYPE and want the response to look like this
{
status : "success",
data : {
"accounts" : [
{ "id" : 1, "title" : "saving", "sortcode" : "121212" },
{ "id" : 2, "title" : "current", "sortcode" : "445566" },
]
}
}
So I created a Response<T> object
public class Response<T> {
private String status;
private String message;
T data;
...
...
}
Is this the correct way of doing this, or is there a better way?.
How do you use this Response object in Spring controller to return an empty response object and/or a populated response object.
Thanks in advance GM
UPDATE:
In order to get the similar JSON output as the one described, i.e. with "accounts" key in JSON, I had to use Response<Map<String, List<Account>>> the following in the controller:
#RequestMapping(value = {"/accounts"}, method = RequestMethod.POST, produces = "application/json", headers = "Accept=application/json")
#ResponseBody
public Response<Map<String, List<Account>>> findAccounts(#RequestBody AccountsSearchRequest request) {
//
// empty accounts list
//
List<Account> accountsList = new ArrayList<Account>();
//
// response will hold a MAP with key="accounts" value="List<Account>
//
Response<Map<String, List<Account>>> response = ResponseUtil.createResponseWithData("accounts", accountsList);
try {
accountsList = searchService.findAccounts(request);
response = ResponseUtil.createResponseWithData("accounts", accountsList);
response.setStatus("success");
response.setMessage("Number of accounts ("+accounts.size()+")");
} catch (Exception e) {
response.setStatus("error");
response.setMessage("System error " + e.getMessage());
response.setData(null);
}
return response;
}
Is this the right way of doing this? i.e. in order to get the "accounts" key in JSON output?
While your example JSON is not valid (status and data are not enclosed in quotations), this approach will work.
You will want to ensure that you have the Jackson jars on your classpath, and Spring will take care of the rest.
To get this to work, I would create a constructor for your response class that looks something like this:
public class Response<T> {
private String status;
private String message;
private T data;
public Response(String status, String message, T data) {
this.status = status;
this.message = message;
this.data = data;
}
//...getter methods here
}
And then in your Spring controller, you just return this object from your method that is mapped with #RequestMapping
#Controller
public class MyController {
#RequestMapping(value="/mypath", produces="application/json")
public Response<SomeObject> myPathMethod() {
return new Response<SomeObject>("200", "success!", new SomeObject());
}
}
I'm pretty new to writing Servlet and REST Services, but now i'm at a problem that I'm not sure if i'm doing it correctly. My Service look like this:
#POST
#Produces ("application/json")
#Path ("/register")
public String register(
#FormParam("name") String name,
#FormParam("username") String username,
#FormParam("password") String password,
#Context HttpServletResponse servletResponse) throws IOException {
if( this.user_taken(username) ) return "USERNAME_TAKEN";
User user = new User(name,username,password);
.....
return mapper.writeValueAsString(user);
}
So as you can see the Service takes care of doing the back end (database and creating user) the Servlet on the other hand is in charge of taking request from the form, properly validating and passing it to the Service. Servlet Code:
... validate user input form ...
ClientConfig config = new DefaultClientConfig();
Client client = Client.create(config);
WebResource service = client.resource("http://localhost/Jaba");
String map = mapper.writeValueAsString(request.getParameterMap());
MultivaluedMap<String, String> obj = mapper.readValue(map, MultivaluedMap.class);
String result =
service.path("api").path("register")
.accept("application/json")
.post(String.class, obj);
As you can see the Client (Servlet) has to do a lot of nasty work, to pass data to the Service. How can this be changed/improved/optimized or better yet refactored ? I'm trying to follow best practices and how it would be in a real life example.
Here is what I might do:
Instead of doing
String result =
service.path("api").path("register")
.accept("application/json")
.post(String.class, obj);
I would do something more like creating a DTO object, filling it out and then passing it to your service. This is then were you would apply an aspect along with JSR validation and annotations (you can do this on what you have but it won't be nearly so nice) on the client call.
example:
#Aspect
public class DtoValidator {
private Validator validator;
public DtoValidator() {
}
public DtoValidator(Validator validator) {
this.validator = validator;
}
public void doValidation(JoinPoint jp){
for( Object arg : jp.getArgs() ){
if (arg != null) {
Set<ConstraintViolation<Object>> violations = validator.validate(arg);
if( violations.size() > 0 ){
throw buildError(violations);
}
}
}
}
private static BadRequestException buildError( Set<ConstraintViolation<Object>> violations ){
Map<String, String> errorMap = new HashMap<String, String>();
for( ConstraintViolation error : violations ){
errorMap.put(error.getPropertyPath().toString(), error.getMessage());
}
return new BadRequestException(errorMap);
}
}
You can annotatively declare you aspect or you can do it in config (makes it reusable). As such:
<aop:config proxy-target-class="true">
<aop:aspect id="dtoValidator" ref="dtoValidator" order="10">
<aop:before method="doValidation" pointcut="execution(public * com.mycompany.client.*.*(..))"/>
</aop:aspect>
</aop:config>
Now you can have a DTO like this:
#XmlAccessorType(XmlAccessType.FIELD)
#XmlRootElement
public class LoginRequest extends AbstractDto{
#NotNull
private String userName;
#NotNull
private String password;
private LoginRequest() {
}
public LoginRequest(String userName, String password) {
this.userName = userName;
this.password = password;
}
public String getUserName() {
return userName;
}
public String getPassword() {
return password;
}
}
When it fails those #NotNull checks you will get something like this:
{
"message":"{username=must not be null",
"httpStatusCode":400,
"httpMessage":"Bad Request",
"details":{
"username":"must not be null"
}
}
Then use a RestOperation client as such
org.springframework.web.client.RestOperations restClient ...
restClient.postForObject(URL,new Dto(...),args);
Place the aspect around this restClient and you're golden (and, actually for good measure, on your service calls too).