EDIT: The error was in the client not the server. The response body was getting written, but the client was not reading it on a 400 response.
I have a custom message converter to produce text/csv, application/csv from an ErrorResponse object. It works as expected when the ErrorResponse is returned directly from a #RequestMapping annotated method, but returns no response body when ErrorResponse is return from an #ExceptionHandler annotated method in a #ControllerAdvice object. I have verified that the message converter writerInternal method is being called and is writing to the response body, but is never makes it back to the client.
ErrorResponse:
#XmlAccessorType(XmlAccessType.FIELD)
#XmlRootElement(name="response")
public class ErrorResponse {
private String statusCode;
private String userMessage;
private String developerMessage;
public String getStatusCode() {
return statusCode;
}
public void setStatusCode(final String statusCode) {
this.statusCode = statusCode;
}
public String getUserMessage() {
return userMessage;
}
public void setUserMessage(final String userMessage) {
this.userMessage = userMessage;
}
public String getDeveloperMessage() {
return developerMessage;
}
public void setDeveloperMessage(final String developerMessage) {
this.developerMessage = developerMessage;
}
public ErrorResponse() {
super();
}
public ErrorResponse(final String statusCode, final String userMessage, final String developerMessage) {
super();
this.statusCode = statusCode;
this.userMessage = userMessage;
this.developerMessage = developerMessage;
}
}
MessageConverter:
public class ErrorResponseCsvMessageConverter extends AbstractHttpMessageConverter<ErrorResponse> {
public ErrorResponseCsvMessageConverter() {
super(new MediaType("application", "csv", Charset.forName("UTF-8")),
new MediaType("text", "csv", Charset.forName("UTF-8")),
MediaType.TEXT_PLAIN);
}
#Override
protected ErrorResponse readInternal(final Class<? extends ErrorResponse> clazz, final HttpInputMessage httpInputMessage)
throws IOException, HttpMessageNotReadableException {
// not supported
return null;
}
#Override
protected boolean supports(final Class<?> clazz) {
return ErrorResponse.class.isAssignableFrom(clazz);
}
#Override
protected void writeInternal(final ErrorResponse errorResponse, final HttpOutputMessage httpOutputMessage)
throws IOException, HttpMessageNotWritableException {
System.out.println(errorResponse);
try(CSVWriter csvWriter = new CSVWriter(new OutputStreamWriter(httpOutputMessage.getBody(), "UTF-8"))) {
csvWriter.writeNext(new String[] { "statusCode", "userMessage", "developerMessage" });
csvWriter.writeNext(new String[] {
errorResponse.getStatusCode(),
errorResponse.getUserMessage(),
errorResponse.getDeveloperMessage() });
}
}
}
Controller Advice:
...
#ExceptionHandler(MissingServletRequestParameterException.class)
#ResponseBody()
#ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleMissingParamterException(final HttpServletRequest request, final HttpServletResponse httpServletResponse, final MissingServletRequestParameterException e) {
LOG.warn("Bad Request:" +
request.getRequestURI() +
((request.getQueryString()==null) ? "" : "?" + request.getQueryString()));
return new ErrorResponse(
"400",
"There was an error with the request.",
"Required parameter '" + e.getParameterName() + "' is missing.");
}
...
I think the message is being written but not flushed...
So your converter may be missing something like:
outputMessage.getBody().flush();
Maybe even use Spring's AbstractHttpMessageConverter ?
Related
I use springboot framework. I need to encrypt body data(from json to encrypted string).
So I used ResponseBodyAdvice.java with #ControllerAdvice but there was a problem that it always responds the encrypted data with double quotes(e.g. "hello")
I need to respond just hello instead of "hello".
#Override
public Object beforeBodyWrite(...) {
response.getHeaders().set("content-type", "text/plain;charset=UTF-8");
//some codes..
String result = "hello";
return result;
}
It responds "hello" (I need to the data without double quotes)
In controller class, it responds just hello(without double quotes).
See the below code.
#ApiOperation(value = "absdfasdf", produces = "text/plain")
#GetMapping("/absd")
public String asdfasdf() {
return "hello";
}
You need to check your MessageConverters to make sure StringMessageConverter is before MappingJackson2HttpMessageConverter. Otherwise, JSON MessageConverter will be selected to serialize string and add the extra double quotes.
#Component
public class MyWebMvcConfigurer implements WebMvcConfigurer {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// check converter order here
}
}
As the following spring source code, the flow is:
choose a message converter
invoke beforeBodyWrite method in ResponseBodyAdvice
convert message
AbstractMessageConverterMethodProcessor.java
// choose a message converter
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
// invoke beforeBodyWrite
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn ->
"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
}
else {
// convert message
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
The truth is that we can not change MessageConverter in ResponseBodyAdvice. But we can custom a Dynamic MessageConverter. For example:
public class DynamicMessageConverter implements HttpMessageConverter<Object> {
private final HttpMessageConverter<Object> jsonConverter;
private final HttpMessageConverter<String> stringConverter;
public DynamicMessageConverter(HttpMessageConverter<Object> jsonConverter, HttpMessageConverter<String> stringConverter) {
this.jsonConverter = jsonConverter;
this.stringConverter = stringConverter;
}
#Override
public boolean canWrite(Class clazz, MediaType mediaType) {
return jsonConverter.canWrite(clazz, mediaType) || stringConverter.canWrite(clazz, mediaType);
}
#Override
public List<MediaType> getSupportedMediaTypes() {
List<MediaType> jsonMediaTypes = jsonConverter.getSupportedMediaTypes();
List<MediaType> stringMediaTypes = stringConverter.getSupportedMediaTypes();
List<MediaType> all = new ArrayList<>();
all.addAll(jsonMediaTypes);
all.addAll(stringMediaTypes);
return all;
}
#Override
public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
if (o instanceof String) {
stringConverter.write((String) o, contentType, outputMessage);
} else {
jsonConverter.write(o, contentType, outputMessage);
}
}
#Override
public boolean canRead(Class clazz, MediaType mediaType) {
return false;
}
#Override
public Object read(Class clazz, HttpInputMessage inputMessage) throws HttpMessageNotReadableException {
throw new UnsupportedOperationException();
}
}
And then enable it
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
DynamicMessageConverter dynamicMessageConverter = new DynamicMessageConverter(jsonConverter, stringConverter);
converters.add(0, dynamicMessageConverter);
}
Writing directly through response seems more concise.
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
response.getHeaders().set("content-type", "text/plain;charset=UTF-8");
//some codes..
String result = "hello";
try (OutputStream stream = response.getBody()) {
stream.write(result.getBytes("utf-8"));
stream.flush();
} catch (IOException e) {
// log ex
}
return null;
}
I am trying to send POST Request from Android to Spring via Retrofit from past 2 days. I have tried plenty of solutions regarding this, but nothing seems to be working. So, asking here finally to be able to get some help.
So, i am trying to send a simple Object from Android to Spring via retrofit. From android side i have verified the values sent by me and it gives me correct value.(I have debugged it via Android's debugging mode). But on spring side i got null values.
This is my code ->
function foo() -> from which i am sending my request.
private void foo() {
request.setName1("XYZ");
request.setName2("PQR");
Api.upload(request, new Callback<BasicResponse>() {
#Override
public void onResponse(Call<BasicResponse> call, Response<BasicResponse> response) {
Log.d(TAG, "onResponse: Success" + response.toString());
}
#Override
public void onFailure(Call<BasicResponse> call, Throwable t) {
Log.d(TAG, "onResponse: Failure");
}
});
}
}
my upload Function ->
public static void upload(Request request, Callback<BasicResponse> callback) {
uploadRequest api = retrofit.create(uploadRequest.class);
Call<BasicResponse> call = uploadRequest.uploadCall(POJO);
call.enqueue(callback);
}
This is my UploadRequest Interface ->
public interface uploadRequest{
#POST(UPLOAD_REQUEST)
Call<BasicResponse> uploadCall(#Body POJO pojo);
}
This is My POJO Class
public class POJO {
private String Name1;
private String Name2;
public String getName1() {
return Name1;
}
public void setName1(String name1) {
Name1 = name1;
}
public String getName2() {
return Name2;
}
public void setName2(String name2) {
Name2 = name2;
}
}
And this is my Spring Controller Method
#RequestMapping(value = "/UploadRequest",method = RequestMethod.POST,consumes = "application/json", produces = "application/json")
#ResponseBody
public void UploadImage(#RequestBody POJO pojo,HttpServletRequest request) {
if(pojo!=null){
System.out.println("pojo is not null");
System.out.println(pojo.getName1());
}
else{
System.out.println("null");
}
}
I am getting pojo is not null and inside the pojo.getName1(), the value prints is null.
Edit : Adding BasicResponse Class.
public class BasicResponse {
private boolean success = Boolean.TRUE;
private ErrorCode errorCode;
private String response;
private Integer totalCount;
private String callingUserId;
public BasicResponse() {
super();
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public ErrorCode getErrorCode() {
return errorCode;
}
public void setErrorCode(ErrorCode errorCode) {
this.errorCode = errorCode;
this.success = false;
}
public String getResponse() {
return response;
}
public void setResponse(String response) {
this.response = response;
}
public void setResponse(Object response) {
if (response != null) {
this.response = GsonUtils.toGson(response);
}
}
public Integer getTotalCount() {
return totalCount;
}
public void setTotalCount(Integer totalCount) {
this.totalCount = totalCount;
}
public String getCallingUserId() {
return callingUserId;
}
public void setCallingUserId(String callingUserId) {
this.callingUserId = callingUserId;
}
}
Compare the response and your POJO class. The instance variables of your POJO class must be the same as in response. In your case Name1 and Name2. If they are name1, name2 in the response (which means if they do not start with capital letters, etc.), or different, it gives you NullPointerException.
Most likely Name1 and Name2 have different names in json than in your POJO and Jackson, with is used under the hood when you annotate your parameters with #RequestBody can not figure them out, so you get null values. Check out your json.
I am trying to map JSON response as below:
{
object: {
id: 1
name: "my name"
email: "username#mail.com"
username: "username"
password: "password"
mobile: "##########"
fbAccessToken: "----------"
img: null
}
errorMessage: ""
successMessage: ""
technicalErrorMessage: ""
error: false
}
so I wrote this method:
private <T> ResponseEntity<T> processedRequest(HttpRequestBase requestBase, Class<T> tClass) throws IOException {
HttpResponse response = httpClient.execute(requestBase);
HttpEntity entity = response.getEntity();
Reader reader = new InputStreamReader(entity.getContent());
Type type = new TypeToken<ResponseEntity<T>>() {}.getType();
ResponseEntity<T> responseEntity = gson.fromJson(reader, type);
return responseEntity;
}
based on ResponseEntity class:
public class ResponseEntity<T> {
private T object;
private boolean isError;
private String errorMessage;
private String successMessage;
private String technicalErrorMessage;
public ResponseEntity() {
setSuccessMessage("");
setError(false);
setErrorMessage("");
setTechnicalErrorMessage("");
}
public T getObject() {
return object;
}
public void setObject(T object) {
this.object = object;
}
public boolean isError() {
return isError;
}
public void setError(boolean error) {
this.isError = error;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public String getTechnicalErrorMessage() {
return technicalErrorMessage;
}
public void setTechnicalErrorMessage(String technicalErrorMessage) {
this.technicalErrorMessage = technicalErrorMessage;
}
public String getSuccessMessage() {
return successMessage;
}
public void setSuccessMessage(String successMessage) {
this.successMessage = successMessage;
}}
but I am getting result as ResponseEntity<LinkedTreeMap> and the object is map of (Key->Value) not the actual mapped object that send by the Type ResponseEntity<T>.
The image below is what appeared in the debugger:
How GSON should understand, which class it should use in place of T and what is the "actual mapped object that send by the Type" to fill it's fields? We have type erasure for generics in Java, so no way in runtime to understand what it T. No way, so gson just uses generic Map<String, String>().
Take a look at responses to this question, it's the same situation.
This question already has answers here:
Configuring Spring MVC controller to send file to client
(2 answers)
Closed 8 years ago.
Given a simple Java Object:
public class Pojo {
private String x;
private String y;
private String z;
//... getters/setters ...
}
Is there some lib that i can put on my project that will make a controller like this:
#RequestMapping(value="/csv", method=RequestMethod.GET, produces= MediaType.TEXT_PLAIN)
#ResponseBody
public List<Pojo> csv() {
//Some code to get a list of Pojo objects
//...
return myListOfPojos;
}
To produce a csv file of my Pojos? For a Json result, i use Jackson lib. I need another lib for CSV results.
As a simple variant. You can generate csv by any way you want and return it as String.
Something like this:
#RequestMapping(value="/csv", method=RequestMethod.GET)
#ResponseBody
public String csv() {
//Some code to get a list of Pojo objects
//...
StringBuilder sb = new StringBuilder();
for (Pojo pojo: myListOfPojos){
sb.append(pojo.getX());
sb.append(",");
sb.append(pojo.getY());
sb.append(",");
sb.append(pojo.getZ());
sb.append("\n");
}
return sb.toString;
}
Should work.
Autogenerate this strings by reflection looks like simple work too.
Based on another question, i did my own HTTPMessageConverter for Tsv Responses.
TsvMessageConverter.java
public class TsvMessageConverter extends AbstractHttpMessageConverter<TsvResponse> {
public static final MediaType MEDIA_TYPE = new MediaType("text", "tsv", Charset.forName("utf-8"));
private static final Logger logger = LoggerFactory.getLogger(TsvMessageConverter.class);
public TsvMessageConverter() {
super(MEDIA_TYPE);
}
protected boolean supports(Class<?> clazz) {
return TsvResponse.class.equals(clazz);
}
#Override
protected TsvResponse readInternal(Class<? extends TsvResponse> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
protected void writeInternal(TsvResponse tsvResponse, HttpOutputMessage output) throws IOException, HttpMessageNotWritableException {
output.getHeaders().setContentType(MEDIA_TYPE);
output.getHeaders().set("Content-Disposition", "attachment; filename=\"" + tsvResponse.getFilename() + "\"");
final OutputStream out = output.getBody();
writeColumnTitles(tsvResponse, out);
if (tsvResponse.getRecords() != null && tsvResponse.getRecords().size() != 0) {
writeRecords(tsvResponse, out);
}
out.close();
}
private void writeRecords(TsvResponse response, OutputStream out) throws IOException {
List<String> getters = getObjectGetters(response);
for (final Object record : response.getRecords()) {
for (String getter : getters) {
try {
Method method = ReflectionUtils.findMethod(record.getClass(), getter);
out.write(method.invoke(record).toString().getBytes(Charset.forName("utf-8")));
out.write('\t');
} catch (IllegalAccessException | InvocationTargetException e) {
logger.error("Erro ao transformar em CSV", e);
}
}
out.write('\n');
}
}
private List<String> getObjectGetters(TsvResponse response) {
List<String> getters = new ArrayList<>();
for (Method method : ReflectionUtils.getAllDeclaredMethods(response.getRecords().get(0).getClass())) {
String methodName = method.getName();
if (methodName.startsWith("get") && !methodName.equals("getClass")) {
getters.add(methodName);
}
}
sort(getters);
return getters;
}
private void writeColumnTitles(TsvResponse response, OutputStream out) throws IOException {
for (String columnTitle : response.getColumnTitles()) {
out.write(columnTitle.getBytes());
out.write('\t');
}
out.write('\n');
}
}
TsvResponse.java
public class TsvResponse {
private final String filename;
private final List records;
private final String[] columnTitles;
public TsvResponse(List records, String filename, String ... columnTitles) {
this.records = records;
this.filename = filename;
this.columnTitles = columnTitles;
}
public String getFilename() {
return filename;
}
public List getRecords() {
return records;
}
public String[] getColumnTitles() {
return columnTitles;
}
}
And on SpringContext.xml add the following:
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="com.mypackage.TsvMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
So, you can use on your controller like this:
#RequestMapping(value="/tsv", method= RequestMethod.GET, produces = "text/tsv")
#ResponseBody
public TsvResponse tsv() {
return new TsvResponse(myListOfPojos, "fileName.tsv",
"Name", "Email", "Phone", "Mobile");
}
I think my scenario is pretty common. I have a database and I want my Spring MVC app to accept a request in the controller, invoke the DB service to get data and send that data to the client as a CSV file. I'm using the JavaCSV library found here to assist in the process: http://sourceforge.net/projects/javacsv/
I've found several examples of people doing similar things and cobbled together something that looks correct-ish. When I hit the method, though, nothing is really happening.
I thought writing the data to the HttpServletResponse's outputStream would be sufficient, but apparently, I'm missing something.
Here's my controller code:
#RequestMapping(value="/getFullData.html", method = RequestMethod.GET)
public void getFullData(HttpSession session, HttpServletRequest request, HttpServletResponse response) throws IOException{
List<CompositeRequirement> allRecords = compReqServ.getFullDataSet((String)session.getAttribute("currentProject"));
response.setContentType("data:text/csv;charset=utf-8");
response.setHeader("Content-Disposition","attachment; filename=\yourData.csv\"");
OutputStream resOs= response.getOutputStream();
OutputStream buffOs= new BufferedOutputStream(resOs);
OutputStreamWriter outputwriter = new OutputStreamWriter(buffOs);
CsvWriter writer = new CsvWriter(outputwriter, '\u0009');
for(int i=1;i <allRecords.size();i++){
CompositeRequirement aReq=allRecords.get(i);
writer.write(aReq.toString());
}
outputwriter.flush();
outputwriter.close();
};
What step am I missing here? Basically, the net effect is... nothing. I would have thought setting the header and content type would cause my browser to pick up on the response and trigger a file download action.
It seems to be because your Content-type is set incorrectly, it should be response.setContentType("text/csv;charset=utf-8") instead of response.setContentType("data:text/csv;charset=utf-8").
Additionally, if you are using Spring 3, you should probably use a #ResponseBody HttpMessageConverter for code reuse. For example:
In the controller:
#RequestMapping(value = "/getFullData2.html", method = RequestMethod.GET, consumes = "text/csv")
#ResponseBody // indicate to use a compatible HttpMessageConverter
public CsvResponse getFullData(HttpSession session) throws IOException {
List<CompositeRequirement> allRecords = compReqServ.getFullDataSet((String) session.getAttribute("currentProject"));
return new CsvResponse(allRecords, "yourData.csv");
}
plus a simple HttpMessageConverter:
public class CsvMessageConverter extends AbstractHttpMessageConverter<CsvResponse> {
public static final MediaType MEDIA_TYPE = new MediaType("text", "csv", Charset.forName("utf-8"));
public CsvMessageConverter() {
super(MEDIA_TYPE);
}
protected boolean supports(Class<?> clazz) {
return CsvResponse.class.equals(clazz);
}
protected void writeInternal(CsvResponse response, HttpOutputMessage output) throws IOException, HttpMessageNotWritableException {
output.getHeaders().setContentType(MEDIA_TYPE);
output.getHeaders().set("Content-Disposition", "attachment; filename=\"" + response.getFilename() + "\"");
OutputStream out = output.getBody();
CsvWriter writer = new CsvWriter(new OutputStreamWriter(out), '\u0009');
List<CompositeRequirement> allRecords = response.getRecords();
for (int i = 1; i < allRecords.size(); i++) {
CompositeRequirement aReq = allRecords.get(i);
writer.write(aReq.toString());
}
writer.close();
}
}
and a simple object to bind everything together:
public class CsvResponse {
private final String filename;
private final List<CompositeRequirement> records;
public CsvResponse(List<CompositeRequirement> records, String filename) {
this.records = records;
this.filename = filename;
}
public String getFilename() {
return filename;
}
public List<CompositeRequirement> getRecords() {
return records;
}
}
Based on Pierre answer, i did a converter. Here is the full code, that works with any Object passed:
TsvMessageConverter.java
public class TsvMessageConverter extends AbstractHttpMessageConverter<TsvResponse> {
public static final MediaType MEDIA_TYPE = new MediaType("text", "tsv", Charset.forName("utf-8"));
private static final Logger logger = LoggerFactory.getLogger(TsvMessageConverter.class);
public TsvMessageConverter() {
super(MEDIA_TYPE);
}
protected boolean supports(Class<?> clazz) {
return TsvResponse.class.equals(clazz);
}
#Override
protected TsvResponse readInternal(Class<? extends TsvResponse> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
protected void writeInternal(TsvResponse tsvResponse, HttpOutputMessage output) throws IOException, HttpMessageNotWritableException {
output.getHeaders().setContentType(MEDIA_TYPE);
output.getHeaders().set("Content-Disposition", "attachment; filename=\"" + tsvResponse.getFilename() + "\"");
final OutputStream out = output.getBody();
writeColumnTitles(tsvResponse, out);
if (tsvResponse.getRecords() != null && tsvResponse.getRecords().size() != 0) {
writeRecords(tsvResponse, out);
}
out.close();
}
private void writeRecords(TsvResponse response, OutputStream out) throws IOException {
List<String> getters = getObjectGetters(response);
for (final Object record : response.getRecords()) {
for (String getter : getters) {
try {
Method method = ReflectionUtils.findMethod(record.getClass(), getter);
out.write(method.invoke(record).toString().getBytes(Charset.forName("utf-8")));
out.write('\t');
} catch (IllegalAccessException | InvocationTargetException e) {
logger.error("Erro ao transformar em CSV", e);
}
}
out.write('\n');
}
}
private List<String> getObjectGetters(TsvResponse response) {
List<String> getters = new ArrayList<>();
for (Method method : ReflectionUtils.getAllDeclaredMethods(response.getRecords().get(0).getClass())) {
String methodName = method.getName();
if (methodName.startsWith("get") && !methodName.equals("getClass")) {
getters.add(methodName);
}
}
sort(getters);
return getters;
}
private void writeColumnTitles(TsvResponse response, OutputStream out) throws IOException {
for (String columnTitle : response.getColumnTitles()) {
out.write(columnTitle.getBytes());
out.write('\t');
}
out.write('\n');
}
}
TsvResponse.java
public class TsvResponse {
private final String filename;
private final List records;
private final String[] columnTitles;
public TsvResponse(List records, String filename, String ... columnTitles) {
this.records = records;
this.filename = filename;
this.columnTitles = columnTitles;
}
public String getFilename() {
return filename;
}
public List getRecords() {
return records;
}
public String[] getColumnTitles() {
return columnTitles;
}
}
And on SpringContext.xml add the following:
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="com.mypackage.TsvMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
So, you can use on your controller like this:
#RequestMapping(value="/tsv", method= RequestMethod.GET, produces = "text/tsv")
#ResponseBody
public TsvResponse tsv() {
return new TsvResponse(myListOfPojos, "fileName.tsv",
"Name", "Email", "Phone", "Mobile");
}