Commit 20fedd96 authored by Maxym Borodenko's avatar Maxym Borodenko Committed by Matija Obreza

WIP: Support for text/csv API response

Supporting for Page and Collection; Custom headers for page
parent a8b6c76a
...@@ -530,7 +530,7 @@ ...@@ -530,7 +530,7 @@
<dependency> <dependency>
<groupId>com.opencsv</groupId> <groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId> <artifactId>opencsv</artifactId>
<version>3.8</version> <version>4.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.liquibase.ext</groupId> <groupId>org.liquibase.ext</groupId>
......
/*
* Copyright 2019 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys2.spring;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.opencsv.CSVWriter;
import org.genesys.blocks.model.BasicModel;
import org.genesys2.util.JsonParser;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
/**
* @author Maxym Borodenko
*/
public class CSVMessageConverter<T> extends AbstractHttpMessageConverter<T> {
private static final MediaType MEDIA_TYPE = new MediaType("text", "csv");
private ObjectMapper mapper;
public CSVMessageConverter(final ObjectMapper mapper) {
super(MEDIA_TYPE);
this.mapper = mapper;
}
@Override
protected boolean supports(final Class<?> clazz) {
return Page.class.isAssignableFrom(clazz) || Collection.class.isAssignableFrom(clazz);
}
@Override
protected T readInternal(final Class<? extends T> clazz, final HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
protected void addDefaultHeaders(final HttpHeaders headers, final T t, final MediaType contentType) throws IOException {
super.addDefaultHeaders(headers, t, contentType);
if (Page.class.isAssignableFrom(t.getClass())) {
headers.add("Pagination-Page", String.valueOf(((PageImpl) t).getNumber()));
headers.add("Pagination-Size", String.valueOf(((PageImpl) t).getSize()));
headers.add("Pagination-Count", String.valueOf(((PageImpl) t).getTotalElements()));
}
}
/**
* Method converts to CSV format and writes data to the output message
*/
@Override
protected void writeInternal(final T t, final HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
List<Map<String, String>> records = new LinkedList<>();
Set<String> paths = new LinkedHashSet<>();
Set<String> initialPathsOfArrays = new HashSet<>();
boolean isPage = Page.class.isAssignableFrom(t.getClass());
Collection<BasicModel> objects = isPage ? ((Page) t).getContent() : (Collection) t;
if (objects != null && objects.size() > 0) {
for (BasicModel object : objects) {
String json = mapper.writeValueAsString(object);
JsonParser parser = new JsonParser(json);
initialPathsOfArrays.addAll(parser.getInitialPathsOfArrays());
Map<String, String> map = parser.getMap();
paths.addAll(map.keySet());
records.add(map);
}
}
paths.removeAll(initialPathsOfArrays);
CSVWriter writer = new CSVWriter(new OutputStreamWriter(outputMessage.getBody()), '\t', '"', '\\', "\n");
// adding header to csv
String[] header = paths.toArray(new String[paths.size()]);
writer.writeNext(header);
for (Map<String, String> row: records) {
List<String> rowValues = new LinkedList<>();
for (String key: header) {
rowValues.add(row.get(key));
}
writer.writeNext(rowValues.toArray(new String[rowValues.size()]));
}
writer.close();
}
}
...@@ -26,6 +26,7 @@ import javax.validation.Validation; ...@@ -26,6 +26,7 @@ import javax.validation.Validation;
import javax.validation.ValidatorFactory; import javax.validation.ValidatorFactory;
import org.genesys2.server.mvc.AddStuffInterceptor; import org.genesys2.server.mvc.AddStuffInterceptor;
import org.genesys2.spring.CSVMessageConverter;
import org.genesys2.spring.RequestAttributeLocaleResolver; import org.genesys2.spring.RequestAttributeLocaleResolver;
import org.genesys2.spring.RequestTrackingInterceptor; import org.genesys2.spring.RequestTrackingInterceptor;
import org.genesys2.spring.validation.oval.spring.SpringOvalValidator; import org.genesys2.spring.validation.oval.spring.SpringOvalValidator;
...@@ -222,7 +223,11 @@ public class WebConfiguration extends WebMvcConfigurerAdapter { ...@@ -222,7 +223,11 @@ public class WebConfiguration extends WebMvcConfigurerAdapter {
public MappingJackson2HttpMessageConverter jacksonMessageConverter() { public MappingJackson2HttpMessageConverter jacksonMessageConverter() {
final MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter(); final MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
messageConverter.setObjectMapper(objectMapper());
return messageConverter;
}
private ObjectMapper objectMapper() {
final ObjectMapper mapper = new ObjectMapper(); final ObjectMapper mapper = new ObjectMapper();
Hibernate4Module hibernateModule = new Hibernate4Module(); Hibernate4Module hibernateModule = new Hibernate4Module();
...@@ -241,9 +246,7 @@ public class WebConfiguration extends WebMvcConfigurerAdapter { ...@@ -241,9 +246,7 @@ public class WebConfiguration extends WebMvcConfigurerAdapter {
// explicit json views: every fields needs to be annotated, therefore enabled // explicit json views: every fields needs to be annotated, therefore enabled
mapper.enable(MapperFeature.DEFAULT_VIEW_INCLUSION); mapper.enable(MapperFeature.DEFAULT_VIEW_INCLUSION);
messageConverter.setObjectMapper(mapper); return mapper;
return messageConverter;
} }
@Override @Override
...@@ -254,6 +257,8 @@ public class WebConfiguration extends WebMvcConfigurerAdapter { ...@@ -254,6 +257,8 @@ public class WebConfiguration extends WebMvcConfigurerAdapter {
converters.add(new ByteArrayHttpMessageConverter()); converters.add(new ByteArrayHttpMessageConverter());
// HTTP string // HTTP string
converters.add(new StringHttpMessageConverter()); converters.add(new StringHttpMessageConverter());
// Custom CSV converter
converters.add(new CSVMessageConverter(objectMapper()));
super.configureMessageConverters(converters); super.configureMessageConverters(converters);
} }
......
/*
* Copyright 2019 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys2.util;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ValueNode;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.StringUtils;
/**
* Converts a JSON to a map of keys (full JSON paths) and values
*
* @author Maxym Borodenko
*/
public class JsonParser {
private ObjectMapper mapper;
private HashSet<String> ignored = Sets.newHashSet("_class", "_permissions");
private HashSet<String> dates = Sets.newHashSet("lastModifiedDate", "createdDate");
private Map<String, String> map = new LinkedHashMap<>();
private Set<String> initialPathsOfArrays = new HashSet<>();
public JsonParser(final String json) {
mapper = new ObjectMapper();
setJsonPaths(json);
}
public Set<String> getInitialPathsOfArrays() {
return this.initialPathsOfArrays;
}
public Map<String, String> getMap() {
return this.map;
}
private void setJsonPaths(final String json) {
JsonNode object = null;
try {
object = mapper.readTree(json);
} catch (IOException ex) {
}
if (object != null) {
readObject(object, "");
}
}
private void readObject(final JsonNode object, final String jsonPath) {
Iterator<String> keysItr = object.fieldNames();
while (keysItr.hasNext()) {
String key = keysItr.next();
if (ignored.contains(key)) {
continue;
}
String parentPath = StringUtils.isEmpty(jsonPath) ? key : jsonPath + "." + key;
if (dates.contains(key)) {
ValueNode valueNode = (ValueNode) object.get(key);
if (! valueNode.isNull()) {
String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date(valueNode.asLong()));
this.map.put(parentPath, date);
} else {
this.map.put(parentPath, "");
}
continue;
}
readValue(object.get(key), parentPath);
}
}
private void readArray(final ArrayNode array, final String jsonPath) {
this.initialPathsOfArrays.add(jsonPath);
for (int i = 0; i < array.size(); i++) {
if (array.size() > 0) {
readValue(array.get(i), jsonPath + (i + 1));
} else {
this.map.put(jsonPath, "");
}
}
}
private void readValue(final Object value, final String jsonPath) {
if (value instanceof ArrayNode) {
readArray((ArrayNode) value, jsonPath);
} else if (value instanceof ObjectNode) {
readObject((ObjectNode) value, jsonPath);
} else if (value instanceof ValueNode) {
ValueNode valueNode = (ValueNode) value;
this.map.put(jsonPath, valueNode.isNull() ? "" : valueNode.asText());
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment