I've been developing a cloud app to mess a little with Spring Cloud and such. Now I'm stuck trying to send a POST or a PUT request to a Spring Data Rest backend using the RestTemplate API but everything I tried ends with an error: HttpMessageNotReadableException: Cannot deserialize instance of java.lang.String out of START_OBJECT token, HttpMessageNotReadableException: Could not read document: Can not deserialize instance of java.lang.String out of START_ARRAY token, ...from request with content type of application/xml;charset=UTF-8!, Error 400 null... you name it. After researching I discovered that it actually is quite hard to consume HAL JSON with RestTemplate (level 3 JSON hypermedia if I recall correctly) but I want to know if it is possible.
I'd like to see some working (detailed if possible) examples of a RestTemplate sending POST and PUT to a Spring Data Rest backend.
Edit: I tried postForEntity, postForLocation, exchange and it just ended in different kinds of errors. Those are some snippets I tried (there're more, it's just that I dispose them).
My entity:
#Entity
public class Account implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#NotNull
#NotEmpty
private String username;
#NotNull
#NotEmpty
private String authorities;
#NotNull
#NotEmpty
private String password;
//Constructor, getter and setter
Some restTemplate attemps:
public Account create(Account account) {
//Doesnt work :S
MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
map.add("name", account.getName());
map.add("username", account.getUsername());
map.add("password", account.getPassword());
map.add("authorities", account.getAuthorities());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
final HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<MultiValueMap<String, String>>(map,
headers);
return restTemplate.exchange(serviceUrl + "/accounts", HttpMethod.POST, entity, Account.class).getBody();
}
//Also tried with a AccountResource which extends from ResourceSupport and doesn't work either. This one gives me a error saying it cannot deserialize Account["name"].
Also tried like this and got an error about header being application/xml: RestTemplate POSTing entity with associations to Spring Data REST server
The other ones just repeat one of those errors.
You need to configure your RestTemplate so it can consume the application/hal+json content type.
It has already been addressed in some other posts, such as this one or that one, and on a bunch of blog posts, such as here.
The following solution works for a Spring Boot project:
First, configure your RestTemplate using a bean:
// other import directives omitted for the sake of brevity
import static org.springframework.hateoas.MediaTypes.HAL_JSON;
#Configuration
public class RestTemplateConfiguration {
#Autowired
private ObjectMapper objectMapper;
/**
*
* #return a {#link RestTemplate} with a HAL converter
*/
#Bean
public RestTemplate restTemplate() {
// converter
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(Arrays.asList(HAL_JSON));
converter.setObjectMapper(objectMapper);
RestTemplate restTemplate = new RestTemplate(Collections.singletonList(converter));
return restTemplate;
}
}
Then, let Spring inject the RestTemplate where you need to consume the REST backend, and use one of the many variants of RestTemplate#exchange:
#Autowired
public RestTemplate restTemplate;
...
// for a single ressource
// GET
Account newAccount = restTemplate.getForObject(url, Account.class);
// POST
Account newAccount = restTemplate.exchange(serviceUrl + "/accounts", HttpMethod.POST, entity, Account.class).getBody();
// or any of the specialized POST methods...
Account newAccount = restTemplate.postForObject(serviceUrl + "/accounts", entity, Account.class);
For a collection, you will manipulate a PagedResources
// for a collection
ParameterizedTypeReference<PagedResources<Account>> responseType =
new ParameterizedTypeReference<PagedResources<Account>>() {};
// GET
PagedResources<Account> accounts =
restTemplate.exchange(url, HttpMethod.GET, null, responseType).getBody();
//
Related
I want to create a REST API to place Files using Spring-Boot. Together with the file I need to get some Metadata which I would like to get as JSON class. At fist I created a class for the metadata:
#Getter
#Setter
#ToString
#RequiredArgsConstructor
#Entity
#Table(name = FileData.TABLE_NAME)
public class FileData {
public static final String TABLE_NAME = "file_data";
private static final long serialVersionUID = 1342709296259278462L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#GenericGenerator(name = "native", strategy = "native")
#Column(updatable = false)
private Long id;
#Column(updatable = false)
private String userId;
#Column(updatable = false, nullable = false)
private String filename;
private String description;
}
Then I created an API:
#RequestMapping("/files")
public interface FileApi {
#Operation(summary = "Add a new File and return its id.", tags = {"File Upload"})
#PostMapping(path = "/upload",
consumes = {MediaType.MULTIPART_FORM_DATA})
ResponseEntity<String> addFile(#RequestPart(value = "file") final MultipartFile file, #RequestBody FileData fileData, #Context final HttpServletRequest request);
}
I also implemented the method in my Controller. Now I am failing to implement a test for this method. I did following:
#Test
public void uploadFile_success() throws Exception {
final MockMultipartFile file = new MockMultipartFile("file", "test.zip", "text/plain", "test".getBytes());
FileData fileData = new FileData();
fileData.setFilename("bla.zip");
fileData.setDescription("A very nice File");
String address = "/files/upload";
mockMvc.perform(MockMvcRequestBuilders.multipart(address)
.file(file)
.content(MAPPER.writeValueAsString(fileData))
.contentType(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().isOK())
.andExpect(content().string("OK"))
.andReturn();
}
When I run the test, I get HTTP error code 415 and the following error message:
Content type 'multipart/form-data' not supported
Regarding Swagger UI the Request Body shall have multipart/fomr-data. I also removed the contentType call (with same result) and changed it to "application/json" (same result, but with this content type).
Can andybody tell me what content type I have to set?
I could run the test with contentType "application/json" by removing the consumes entry in PostMapping, but in Swagger UI I have then to place the data of the file in a string field in JSON data and cannot transfer a binary file. I think this is not the right way to do it...
You might want to skip to my UPDATE 2 bellow
I have a RestController that works, because when I access it directly from the browser, it returns a JSON response. However, when I send a request from a Service in a different bounded context, I get the error:
{"timestamp":1579095291446,"message":"Error while extracting response for type
[class com.path.to.contexttwo.client.dto.WorkerDetails] and content type [application/json]; nested exception is
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error:
Unexpected character ('<' (code 60)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false');
nested exception is com.fasterxml.jackson.core.JsonParseException:
Unexpected character ('<' (code 60)):
expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')\n at [Source: (PushbackInputStream);
line: 1, column: 2]","details":"uri=/context-two/register-new"}
Here is my code:
RestController
package com.path.to.contextone.aplication.presentation;
#RestController
#RequestMapping(path = "/iacess", produces = "application/json")
#CrossOrigin(origins = "*")
public class IAccessRestController {
UserRepository userRepo;
IAcessService iaccessService;
EntityLinks entityLinks;
#Autowired
public IAccessRestController(
UserRepository userRepo,
IAcessService iaccessService,
EntityLinks entityLinks) {
this.userRepo = userRepo;
this.iaccessService= iaccessService;
this.entityLinks = entityLinks;
}
#GetMapping("/get-worker-details/{userName}")
public WorkerDetails getWorkerDetails(#PathVariable String userName) {
User user = userRepo.findByUsername(userName);
WorkerDetails workerDetails = new WorkerDetails();
workerDetails.setUserId(userId);
workerDetails.setGender(user.gender());
workerDetails.setFirstName(user.getFirstName());
workerDetails.setLastName(user.getLastName());
workerDetails.setPhoneNumber(user.getPhoneNumber());
if (workerDetails != null) {
return workerDetails;
}
return null;
}
}
RestClient
package com.path.to.contexttwo.client;
// imports omitted, as well as other code
#Service
public class IAcessRestClientImpl implements IAcessRestClient {
private final RestTemplate restTemplate;
#Autowired
public IAcessRestClientImpl(
final RestTemplate restTemplate
) {
this.restTemplate = restTemplate;
}
#Override
public WorkerDetails getWorkerDetailsByName(final String userName) throws URISyntaxException {
Map<String,String> urlVariables = new HashMap<>();
urlVariables.put("userName", userName);
return restTemplate.getForObject(
"http://localhost:8080/iacess/get-worker-details/{userName}",
WorkerDetails.class,
urlVariables
);
}
}
Config
package com.path.to.contexttwo.configuration;
#Configuration
#EnableWebMvc
public class RestClientConfig {
#Bean
public RestTemplate restTemplate() {
final RestTemplate restTemplate = new RestTemplate();
List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
List<MediaType> mediaTypes = new ArrayList<MediaType>();
mediaTypes.add(MediaType.APPLICATION_JSON);
converter.setSupportedMediaTypes(mediaTypes);
messageConverters.add(converter);
restTemplate.setMessageConverters(messageConverters);
restTemplate.getInterceptors().add(new JsonInterceptor());
return restTemplate;
}
}
WorkerDetails
package com.path.to.contexttwo.client.dto;
import java.io.Serializable;
import java.util.Objects;
public class WorkerDetails implements Serializable {
private long userId;
private String gender;
private String firstName;
private String lastName;
private String phoneNumber;
public WorkerDetails() {
this.userId = -1;
this.gender = null;
this.firstName = null;
this.lastName = null;
this.phoneNumber = null;
}
// omitted all args constructor, getters, setters, equals, hascode, toString for simplicity
}
WorkerDetails also exists in package com.path.to.contextone.ohs_pl;
I've been trying for 3 days, reading and debugging, to no avail. Debugger seems to show that the error happens when RestTemplate is analysing the WorkerDetails.class.
I also tried using ComponentScan in all configuration classes, because files are in separate packages (bounded contexts), without success.
I could just use the UserDetailsRepository from the class that calls IAcessRestClient to get the WorkerDetails, but this would make two different bounded contexts depend on the same database schema.
Any help would be very appreciated.
I can post aditional code per request.
Thanks in advance
UPDATE
#S B ask for input params. here goes the class that sends the params:
CompanyServiceImpl
package com.path.to.contexttwo.domain.services;
// imports
#Service
public class CompanyServiceImpl implements CompanyService {
private CompanyRepository companyRepository;
private CompanyWorkerRepositoery companyWorkerRepositoery;
private WorkerDetailsClient workerDetailsClient;
private WebApplicationContext applicationContext;
#Autowired
CompanyServiceImpl (
CompanyRepository companyRepository,
CompanyWorkerRepositoery companyWorkerRepositoery,
WorkerDetailsClient workerDetailsClient,
WebApplicationContext applicationContext
) {
this.companyRepository = companyRepository;
this.companyWorkerRepositoery = companyWorkerRepositoery;
this.workerDetailsClient = workerDetailsClient;
this.applicationContext = applicationContext;
}
#Transactional
public Company criateCompany(CompanyDTO dto) throws URISyntaxException {
if (dto.getLegalyAuthorized() == true && dto.getTerms() == true) {
Company company = new Company(
dto.getCompanyName(),
dto.getStateId()
);
company = CompanyRepository.save(company);
// when saving the company, we also need some details from the current logged in user which can be
// retrieved from the idendity and access bounded context. We need those details to be saved in this context
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String name = auth.getName();
WorkerDetails workerDetails = WorkerDetailsClient.getWorkerDetailsByName(
name
);
// ... we can't reach the rest of the code anyway, so we omitted
}
}
And here is the response I get when acessing the RestController directly:
{"userId":127,"gender":"M","firstName":"Primeiro","lastName":"Ăšltimo","phoneNumber":"922222222"}
UPDATE 2
Commented out .anyRequest().authenticated() and everything runned OK! So, it has to do with Spring Security all this time. What a shame. Will now try to make things work with security enabled. I was receiving HTML as response because of redirection to login page. Implemented authentication correctly (token request with basic auth) and everything works well.
Thank you all!
Try:
return restTemplate.getForObject(
"http://localhost:8080/iacess/get-worker-details/" + userName,
WorkerDetails.class);
I am using spring boot version = 1.5.2.RELEASE.
When I am sending multi part file with json object to upload file in postman, It throwing 415 Unsupported Media Type exception.
This is my controller class.
#RestController
#RequestMapping("/service/promotion/")
public class JobController {
....
....
....
#RequestMapping(value = "/job/test", method = RequestMethod.POST, produces = "application/json", consumes = "multipart/form-data")
public ResponseEntity<Object> createJobTest(#Valid #RequestBody JobRequest jobRequest,
#RequestParam(value = "file", required = false) MultipartFile multiPartFile) throws Exception {
My json request class.
public class JobRequest {
private String campaignKey;
private String communicationId;
private Integer channelId;
private String templateType;
private String subject;
private String frequencyControl;
private Integer leadsRequested;
private String keywordRelavance;
private String scheduledAt;
private String file;
private String updatedBy;
//getter and setter
}
Json request in postman
Multipart file request in postman
Header Content-type
But when I removed consumes from controller class and from postman as well like
#RequestMapping(value = "/job/test", method = RequestMethod.POST, produces = "application/json")
then debugger coming in controller class but multi part file value coming
null in request object like
I googled a lot there are many similar questions which already posted but none of them helped me.
Please help me to sort out this mystery.
Thank you.
Check this File upload along with other object in Jersey restful web service
Another way is you can pass whole object in text like you are passing file in form-data and convert in object.
#RequestMapping(value = "/uploadDocs", method = RequestMethod.POST, produces = "application/json", consumes = "multipart/form-data")
public ResponseEntity<Object> methodName(#RequestParam("files") MultipartFile file, #RequestParam("anyKeyName") String objectString)
Than you can convert string to object using
Class object = new ObjectMapper().readValue(objectString, Class.class);
I have this Spring Repository:
public interface MessageRepository extends CrudRepository<MessageObject, String>{
public List<MessageObject> findByEmisorOrDestinatario(String emisor, String destinatario);
}
My DAO is:
#Entity
#Table(name = "messages")
public class MessageObject implements Serializable{
private static final long serialVersionUID = 1L;
#Id
private String id;
private String emisor;
private String destinatario;
private String mensaje;
private String tipo;
#JsonFormat(pattern="yyyy-MM-dd")
private LocalDate fecha;
private String id_housing;
public MessageObject() {
}
Now in my Controller I want to receive the Get request and search in my DB so:
#RestController
public class Controller {
#Autowired
private MessageRepository daoMsg;
#RequestMapping(value = "/Mensajes", method = RequestMethod.GET, produces=MediaType.APPLICATION_JSON_VALUE)
public List<MessageObject> enviados (#RequestParam("mail") String mail) {
return daoMsg.findByEmisorOrDestinatario(mail, mail);
}
}
Now I can call the service from my client, so:
ClientConfig config = new ClientConfig();
Client client = ClientBuilder.newClient();
WebTarget webResource = client.target("http://localhost:8082").path("/Mensajes").queryParam(mail);
Invocation.Builder invocationBuilder = webResource.request(MediaType.APPLICATION_JSON);
Response respuesta = invocationBuilder.get();
int status = respuesta.getStatus();
System.out.println(status);
MessageObject[] listMessages = respuesta.readEntity(MessageObject[].class);
Problems: I'm receiving a 400 status code. Also an error deserializing entityRead. Doing the request with Postman returns no errors and return the list of objects in JSON format.
StackTrace:
javax.ws.rs.ProcessingException: Error deserializing object from entity
stream. Caused by: javax.json.bind.JsonbException: Can't create instance of
a class: class [LMessages.MessageObject;
No default constructor found. Caused by: java.lang.NoSuchMethodException:
[LMessages.MessageObject;.<init>()
Question: how can I know where is my code failing? am I using the service invocation well?
Things I tried: changing Mediatype to GenericType
EDIT I tried removing the / from the path, still getting status 400
Solved. Problem was I was using .queryparam without key-value structure. So changing .queryparam(mail) to .queryparam("mail", mail) solved it.
Try to test with this :
#RestController
public class Controller {
#Autowired
private MessageRepository daoMsg;
#RequestMapping(value = "/Mensajes", method = RequestMethod.GET, produces=MediaType.APPLICATION_JSON_VALUE)
#ReponseBody
public ResponseEntity<List<MessageObject>> enviados (#RequestParam("mail") String mail) {
return new ResponseEntity<>(daoMsg.findByEmisorOrDestinatario(mail, mail), HttpStatus.OK);
}
In a Spring Boot controller, I am receiving json and want to "forward" it without any processing:
#RequestMapping(value = "/forward", method = RequestMethod.POST)
public void abc(#RequestBody GeneralJsonRepresentation json, HttpServletRequest request) {
restTemplate.postForEntity(endpoint, json, Object.class)
}
Is it possible to accomplish this, for instance with an implementation of GeneralJsonRepresentation, assuming the controller has no knowledge of the json format and that the received content type is application/json?
You may not even need the GeneralJsonRepresentation if you just use a String.
I created a small working snippet:
#RequestMapping(path="/forward", method = RequestMethod.POST)
public ResponseEntity<String> forward(#RequestBody String postData) {
// maybe needed configuration
final RestTemplate restTemplate = new RestTemplateBuilder().basicAuthorization("user", "password").build();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(postData, headers);
final String targetUrl = "http://targethost/endpoint";
final ResponseEntity<String> response = restTemplate.postForEntity(targetUrl, entity, String.class);
return ResponseEntity.created(...).build();
}