/* (c) 2017 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.importer.rest.converters;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.logging.LogRecord;
import org.geoserver.catalog.AttributeTypeInfo;
import org.geoserver.catalog.CoverageStoreInfo;
import org.geoserver.catalog.DataStoreInfo;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.catalog.StoreInfo;
import org.geoserver.catalog.StyleInfo;
import org.geoserver.config.util.XStreamPersister;
import org.geoserver.config.util.XStreamPersister.Callback;
import org.geoserver.config.util.XStreamPersisterFactory;
import org.geoserver.importer.Database;
import org.geoserver.importer.Directory;
import org.geoserver.importer.FileData;
import org.geoserver.importer.ImportContext;
import org.geoserver.importer.ImportData;
import org.geoserver.importer.ImportTask;
import org.geoserver.importer.Importer;
import org.geoserver.importer.RemoteData;
import org.geoserver.importer.SpatialFile;
import org.geoserver.importer.Table;
import org.geoserver.importer.mosaic.Granule;
import org.geoserver.importer.mosaic.Mosaic;
import org.geoserver.importer.rest.ImportWrapper;
import org.geoserver.importer.transform.AttributeRemapTransform;
import org.geoserver.importer.transform.AttributesToPointGeometryTransform;
import org.geoserver.importer.transform.CreateIndexTransform;
import org.geoserver.importer.transform.DateFormatTransform;
import org.geoserver.importer.transform.GdalAddoTransform;
import org.geoserver.importer.transform.GdalTranslateTransform;
import org.geoserver.importer.transform.GdalWarpTransform;
import org.geoserver.importer.transform.ImportTransform;
import org.geoserver.importer.transform.IntegerFieldToDateTransform;
import org.geoserver.importer.transform.ReprojectTransform;
import org.geoserver.importer.transform.TransformChain;
import org.geoserver.importer.transform.VectorTransformChain;
import org.geoserver.rest.RequestInfo;
import org.geoserver.rest.RestException;
import org.geoserver.rest.converters.BaseMessageConverter;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.stereotype.Component;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import net.sf.json.JSONSerializer;
import net.sf.json.util.JSONBuilder;
/**
* {@link BaseMessageConverter} implementation for writing JSON or HTML responses from {@link ImportContext},
* {@link ImportTask} or {@link ImportWrapper} objects.
*/
@Component
public class ImportJSONWriter {
static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
static {
DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
}
Importer importer;
private Callback callback;
@Autowired
public ImportJSONWriter(Importer importer) {
this.importer = importer;
}
/**
* Determines the number of levels to expand the JSON result, by parsing the "expand" parameter from the
* query map.
*
* @param def The default value to fall back on
* @return
*/
public int expand(int def) {
String ex = null;
Map<String, String[]> queryMap = RequestInfo.get().getQueryMap();
if (queryMap != null) {
String[] params = queryMap.get("expand");
if (params != null && params.length > 0) {
ex = params[0];
}
}
return expand(ex, def);
}
/**
* Determines the number of levels to expand the JSON result
*
* @param expand The value of the "expand" parameter
* @param def The default value to fall back on
* @return
*/
public int expand(String expand, int def) {
if (expand == null) {
return def;
}
try {
return "self".equalsIgnoreCase(expand) ? 1
: "all".equalsIgnoreCase(expand) ? Integer.MAX_VALUE
: "none".equalsIgnoreCase(expand) ? 0 : Integer.parseInt(expand);
} catch (NumberFormatException e) {
return def;
}
}
public void contexts(FlushableJSONBuilder json, Iterator<ImportContext> contexts, int expand) throws IOException {
json.object().key("imports").array();
while (contexts.hasNext()) {
ImportContext context = contexts.next();
context(json, context, false, expand);
}
json.endArray().endObject();
json.flush();
}
public void context(FlushableJSONBuilder json, ImportContext context, boolean top, int expand) throws IOException {
if (top) {
json.object().key("import");
}
json.object();
json.key("id").value(context.getId());
json.key("href").value(RequestInfo.get().servletURI(pathTo(context)));
json.key("state").value(context.getState());
if (context.getMessage() != null) {
json.key("message").value(context.getMessage());
}
if (expand > 0) {
json.key("archive").value(context.isArchive());
if (context.getTargetWorkspace() != null) {
json.key("targetWorkspace").value(toJSON(context.getTargetWorkspace()));
}
if (context.getTargetStore() != null) {
json.key("targetStore");
store(json, context.getTargetStore(), null, false, expand - 1);
// value(toJSON(context.getTargetStore()));
}
if (context.getData() != null) {
json.key("data");
data(json, context.getData(), context, expand - 1);
}
if (!context.getDefaultTransforms().isEmpty()) {
transforms(json, context, expand - 1, context.getDefaultTransforms());
}
tasks(json, context.getTasks(), false, expand - 1);
}
json.endObject();
if (top) {
json.endObject();
}
json.flush();
}
public void tasks(FlushableJSONBuilder json, List<ImportTask> tasks, boolean top, int expand) throws IOException {
if (top) {
json.object();
}
json.key("tasks").array();
for (ImportTask task : tasks) {
task(json,task, false, expand);
}
json.endArray();
if (top) {
json.endObject();
}
json.flush();
}
public void task(FlushableJSONBuilder json, ImportTask task, boolean top, int expand) throws IOException {
long id = task.getId();
String href = RequestInfo.get().servletURI(pathTo(task));
if (top) {
json.object().key("task");
}
json.object();
json.key("id").value(id);
json.key("href").value(href);
json.key("state").value(task.getState());
if (expand > 0) {
json.key("updateMode").value(task.getUpdateMode().name());
// data (used to be source)
ImportData data = task.getData();
if (data != null) {
json.key("data");
data(json, data, task, expand - 1);
}
// target
StoreInfo store = task.getStore();
if (store != null) {
json.key("target");
store(json, store, task, false, expand - 1);
}
json.key("progress").value(href + "/progress");
LayerInfo layer = task.getLayer();
if (layer != null) {
// @todo don't know why catalog isn't set here, thought this was set during load from BDBImportStore
layer.getResource().setCatalog(importer.getCatalog());
json.key("layer");
layer(json, task, false, expand - 1);
}
if (task.getError() != null) {
json.key("errorMessage").value(concatErrorMessages(task.getError()));
}
transformChain(json, task, false, expand - 1);
messages(json, task.getMessages());
}
json.endObject();
if (top) {
json.endObject();
}
json.flush();
}
public void store(FlushableJSONBuilder json, StoreInfo store, ImportTask task, boolean top, int expand) throws IOException {
String type = store instanceof DataStoreInfo ? "dataStore"
: store instanceof CoverageStoreInfo ? "coverageStore" : "store";
json.object();
if (task != null) {
json.key("href").value(RequestInfo.get().servletURI(pathTo(task) + "/target"));
}
if (expand > 0) {
JSONObject obj = toJSON(store);
json.key(type).value(obj.get(type));
} else {
json.key(type).object().key("name").value(store.getName()).key("type")
.value(store.getType()).endObject();
}
json.endObject();
json.flush();
}
public void layer(FlushableJSONBuilder json, ImportTask task, boolean top, int expand) throws IOException {
if (top) {
json.object().key("layer");
}
LayerInfo layer = task.getLayer();
ResourceInfo r = layer.getResource();
json.object().key("name").value(layer.getName()).key("href")
.value(RequestInfo.get().servletURI(pathTo(task) + "/layer"));
if (expand > 0) {
if (r.getTitle() != null) {
json.key("title").value(r.getTitle());
}
if (r.getAbstract() != null) {
json.key("abstract").value(r.getAbstract());
}
if (r.getDescription() != null) {
json.key("description").value(r.getDescription());
}
json.key("originalName").value(task.getOriginalLayerName());
if (r != null) {
json.key("nativeName").value(r.getNativeName());
if (r.getSRS() != null) {
json.key("srs").value(r.getSRS());
}
if (r.getNativeBoundingBox() != null) {
json.key("bbox");
bbox(json, r.getNativeBoundingBox());
}
}
if (r instanceof FeatureTypeInfo) {
featureType(json, (FeatureTypeInfo) r);
}
StyleInfo s = layer.getDefaultStyle();
if (s != null) {
style(json, s, task, false, expand - 1);
}
}
json.endObject();
if (top) {
json.endObject();
}
json.flush();
}
void featureType(FlushableJSONBuilder json, FeatureTypeInfo featureTypeInfo) throws IOException {
json.key("attributes").array();
List<AttributeTypeInfo> attributes = featureTypeInfo.attributes();
for (int i = 0; i < attributes.size(); i++) {
AttributeTypeInfo att = attributes.get(i);
json.object();
json.key("name").value(att.getName());
json.key("binding").value(att.getBinding().getName());
json.endObject();
}
json.endArray();
}
void style(FlushableJSONBuilder json, StyleInfo style, ImportTask task, boolean top, int expand) throws IOException {
if (top) {
json.object();
}
String href = RequestInfo.get().servletURI(pathTo(task) + "/layer/style");
json.key("style");
if (expand > 0) {
JSONObject obj = toJSON(style).getJSONObject("style");
obj.put("href", href);
json.value(obj);
} else {
json.object();
json.key("name").value(style.getName());
json.key("href").value(href);
json.endObject();
}
if (top) {
json.endObject();
}
}
public void transformChain(FlushableJSONBuilder json, ImportTask task, boolean top, int expand) throws IOException {
if (top) {
json.object();
}
TransformChain<? extends ImportTransform> txChain = task.getTransform();
json.key("transformChain").object();
json.key("type").value(txChain instanceof VectorTransformChain ? "vector" : "raster");
transforms(json,task, expand,
txChain != null ? txChain.getTransforms() : new ArrayList<ImportTransform>());
json.endObject();
if (top) {
json.endObject();
}
json.flush();
}
private void transforms(FlushableJSONBuilder json, Object parent, int expand, List<? extends ImportTransform> transforms)
throws IOException {
json.key("transforms").array();
for (int i = 0; i < transforms.size(); i++) {
transform(json,transforms.get(i), i, parent, false, expand);
}
json.endArray();
}
public void transform(FlushableJSONBuilder json, ImportTransform transform, int index, Object parent, boolean top,
int expand) throws IOException {
json.object();
json.key("type").value(transform.getClass().getSimpleName());
json.key("href").value(RequestInfo.get().servletURI(pathTo(parent) + "/transforms/" + index));
if (expand > 0) {
if (transform instanceof DateFormatTransform) {
DateFormatTransform df = (DateFormatTransform) transform;
json.key("field").value(df.getField());
if (df.getDatePattern() != null) {
json.key("format").value(df.getDatePattern().dateFormat().toPattern());
}
} else if (transform instanceof IntegerFieldToDateTransform) {
IntegerFieldToDateTransform df = (IntegerFieldToDateTransform) transform;
json.key("field").value(df.getField());
} else if (transform instanceof CreateIndexTransform) {
CreateIndexTransform df = (CreateIndexTransform) transform;
json.key("field").value(df.getField());
} else if (transform instanceof AttributeRemapTransform) {
AttributeRemapTransform art = (AttributeRemapTransform) transform;
json.key("field").value(art.getField());
json.key("target").value(art.getType().getName());
} else if (transform.getClass() == AttributesToPointGeometryTransform.class) {
AttributesToPointGeometryTransform atpgt = (AttributesToPointGeometryTransform) transform;
json.key("latField").value(atpgt.getLatField());
json.key("lngField").value(atpgt.getLngField());
} else if (transform.getClass() == ReprojectTransform.class) {
ReprojectTransform rt = (ReprojectTransform) transform;
json.key("source").value(srs(rt.getSource()));
json.key("target").value(srs(rt.getTarget()));
} else if (transform.getClass().equals(GdalTranslateTransform.class)) {
GdalTranslateTransform gtx = (GdalTranslateTransform) transform;
List<String> options = gtx.getOptions();
buildJsonOptions(json, "options", options);
} else if (transform.getClass().equals(GdalWarpTransform.class)) {
GdalWarpTransform gw = (GdalWarpTransform) transform;
List<String> options = gw.getOptions();
buildJsonOptions(json, "options", options);
} else if (transform.getClass().equals(GdalAddoTransform.class)) {
GdalAddoTransform gad = (GdalAddoTransform) transform;
List<String> options = gad.getOptions();
buildJsonOptions(json, "options", options);
JSONBuilder arrayBuilder = json.key("levels").array();
for (Integer level : gad.getLevels()) {
arrayBuilder.value(level);
}
arrayBuilder.endArray();
} else {
throw new IOException(
"Serializaiton of " + transform.getClass() + " not implemented");
}
}
json.endObject();
json.flush();
}
private void buildJsonOptions(FlushableJSONBuilder json, String key, List<String> options) {
JSONBuilder arrayBuilder = json.key(key).array();
for (String option : options) {
arrayBuilder.value(option);
}
arrayBuilder.endArray();
}
void bbox(JSONBuilder json, ReferencedEnvelope bbox) {
json.object().key("minx").value(bbox.getMinX()).key("miny").value(bbox.getMinY())
.key("maxx").value(bbox.getMaxX()).key("maxy").value(bbox.getMaxY());
CoordinateReferenceSystem crs = bbox.getCoordinateReferenceSystem();
if (crs != null) {
json.key("crs").value(crs.toWKT());
}
json.endObject();
}
public void data(FlushableJSONBuilder json, ImportData data, Object parent, int expand) throws IOException {
if (data instanceof FileData) {
if (data instanceof Directory) {
if (data instanceof Mosaic) {
mosaic(json, (Mosaic) data, parent, expand);
} else {
directory(json, (Directory) data, parent, expand);
}
} else {
file(json,(FileData) data, parent, expand, false);
}
} else if (data instanceof Database) {
database(json,(Database) data, parent, expand);
} else if (data instanceof Table) {
table(json, (Table) data, parent, expand);
} else if (data instanceof RemoteData) {
remote(json, (RemoteData) data, parent, expand);
}
else {
throw new IllegalArgumentException("Unable to serialize "+data.getClass().getSimpleName()+" as ImportData");
}
json.flush();
}
public void remote(FlushableJSONBuilder json,RemoteData data, Object parent, int expand) throws IOException {
json.object();
json.key("type").value("remote");
json.key("location").value(data.getLocation());
if (data.getUsername() != null) {
json.key("username").value(data.getUsername());
}
if (data.getPassword() != null) {
json.key("password").value(data.getPassword());
}
if (data.getDomain() != null) {
json.key("domain").value(data.getDomain());
}
json.endObject();
json.flush();
}
public void file(FlushableJSONBuilder json,FileData data, Object parent, int expand, boolean href) throws IOException {
json.object();
json.key("type").value("file");
json.key("format").value(data.getFormat() != null ? data.getFormat().getName() : null);
if (href) {
json.key("href").value(RequestInfo.get().servletURI(pathTo(data, parent)));
}
if (expand > 0) {
json.key("location").value(data.getFile().getParentFile().getPath());
if (data.getCharsetEncoding() != null) {
json.key("charset").value(data.getCharsetEncoding());
}
fileContents(json, data, parent, expand);
message(json, data);
} else {
json.key("file").value(data.getFile().getName());
}
json.endObject();
json.flush();
}
void fileContents(FlushableJSONBuilder json, FileData data, Object parent, int expand) throws IOException {
// TODO: we should probably url encode to handle spaces and other chars
String filename = data.getFile().getName();
json.key("file").value(filename);
json.key("href").value(RequestInfo.get().servletURI(pathTo(data, parent) + "/files/" + filename));
if (expand > 0) {
if (data instanceof SpatialFile) {
SpatialFile sf = (SpatialFile) data;
json.key("prj").value(sf.getPrjFile() != null ? sf.getPrjFile().getName() : null);
json.key("other").array();
for (File supp : ((SpatialFile) data).getSuppFiles()) {
json.value(supp.getName());
}
json.endArray();
if (sf instanceof Granule) {
Granule g = (Granule) sf;
if (g.getTimestamp() != null) {
json.key("timestamp").value(DATE_FORMAT.format(g.getTimestamp()));
}
}
}
}
}
public void mosaic(FlushableJSONBuilder json, Mosaic data, Object parent, int expand) throws IOException {
directory(json, data, "mosaic", parent, expand);
}
public void directory(FlushableJSONBuilder json,Directory data, Object parent, int expand) throws IOException {
directory(json,data, "directory", parent, expand);
}
public void directory(FlushableJSONBuilder json,Directory data, String typeName, Object parent, int expand)
throws IOException {
json.object();
json.key("type").value(typeName);
if (data.getFormat() != null) {
json.key("format").value(data.getFormat().getName());
}
json.key("location").value(data.getFile().getPath());
json.key("href").value(RequestInfo.get().servletURI(pathTo(data, parent)));
if (expand > 0) {
if (data.getCharsetEncoding() != null) {
json.key("charset").value(data.getCharsetEncoding());
}
json.key("files");
files(json,data, parent, false, expand - 1);
message(json,data);
}
json.endObject();
json.flush();
}
public void files(FlushableJSONBuilder json, Directory data, Object parent, boolean top, int expand) throws IOException {
if (top) {
json.object().key("files");
}
json.array();
for (FileData file : data.getFiles()) {
json.object();
fileContents(json, file, parent, expand - 1);
json.endObject();
}
json.endArray();
if (top) {
json.endObject();
}
json.flush();
}
public void database(FlushableJSONBuilder json,Database data, Object parent, int expand) throws IOException {
json.object();
json.key("type").value("database");
json.key("format").value(data.getFormat() != null ? data.getFormat().getName() : null);
json.key("href").value(RequestInfo.get().servletURI(pathTo(data, parent)));
if (expand > 0) {
json.key("parameters").object();
for (Map.Entry<String,Serializable> e : data.getParameters().entrySet()) {
json.key(e.getKey()).value(e.getValue());
}
json.endObject();
json.key("tables").array();
for (Table t : data.getTables()) {
json.value(t.getName());
}
message(json,data);
json.endArray();
}
json.endObject();
}
void table(FlushableJSONBuilder json, Table data, Object parent, int expand) throws IOException {
json.object();
json.key("type").value("table");
json.key("name").value(data.getName());
json.key("format").value(data.getFormat() != null ? data.getFormat().getName() : null);
json.key("href").value(RequestInfo.get().servletURI(pathTo(data, parent)));
json.endObject();
}
void message(FlushableJSONBuilder json, ImportData data) throws IOException {
if (data.getMessage() != null) {
json.key("message").value(data.getMessage());
}
}
void messages(FlushableJSONBuilder json, List<LogRecord> records) {
if (!records.isEmpty()) {
json.key("messages");
json.array();
for (int i = 0; i < records.size(); i++) {
LogRecord record = records.get(i);
json.object();
json.key("level").value(record.getLevel().toString());
json.key("message").value(record.getMessage());
json.endObject();
}
json.endArray();
}
}
String concatErrorMessages(Throwable ex) {
StringBuilder buf = new StringBuilder();
while (ex != null) {
if (buf.length() > 0) {
buf.append('\n');
}
if (ex.getMessage() != null) {
buf.append(ex.getMessage());
}
ex = ex.getCause();
}
return buf.toString();
}
FlushableJSONBuilder builder(OutputStream out) {
return new FlushableJSONBuilder(out);
}
JSONObject toJSON(Object o) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
toJSON(o, out);
return (JSONObject) JSONSerializer.toJSON(new String(out.toByteArray()));
}
void toJSON(Object o, OutputStream out) throws IOException {
XStreamPersister xp = persister();
if (callback != null) {
xp.setCallback(callback);
}
xp.save(o, out);
out.flush();
}
XStreamPersister persister() {
XStreamPersister xp = importer
.initXStreamPersister(new XStreamPersisterFactory().createJSONPersister());
xp.setReferenceByName(true);
xp.setExcludeIds();
// xp.setCatalog(importer.getCatalog());
xp.setHideFeatureTypeAttributes();
// @todo this is copy-and-paste from org.geoserver.catalog.rest.FeatureTypeResource
xp.setCallback(new XStreamPersister.Callback() {
@Override
protected void postEncodeFeatureType(FeatureTypeInfo ft,
HierarchicalStreamWriter writer, MarshallingContext context) {
try {
writer.startNode("attributes");
context.convertAnother(ft.attributes());
writer.endNode();
} catch (IOException e) {
throw new RuntimeException("Could not get native attributes", e);
}
}
});
return xp;
}
String srs(CoordinateReferenceSystem crs) {
return CRS.toSRS(crs);
}
static String pathTo(ImportContext context) {
return "/imports/" + context.getId();
}
static String pathTo(ImportTask task) {
return pathTo(task.getContext()) + "/tasks/" + task.getId();
}
String pathTo(Object parent) {
if (parent instanceof ImportContext) {
return pathTo((ImportContext) parent);
} else if (parent instanceof ImportTask) {
return pathTo((ImportTask) parent);
} else {
throw new IllegalArgumentException("Don't recognize: " + parent);
}
}
String pathTo(ImportData data, Object parent) {
return pathTo(parent) + "/data";
}
public static RestException badRequest(String error) {
JSONObject errorResponse = new JSONObject();
JSONArray errors = new JSONArray();
errors.add(error);
errorResponse.put("errors", errors);
return new RestException(errorResponse.toString(), HttpStatus.BAD_REQUEST);
}
public static class FlushableJSONBuilder extends JSONBuilder {
public FlushableJSONBuilder(Writer w) {
super(w);
}
public FlushableJSONBuilder(OutputStream out) {
this(new OutputStreamWriter(out));
}
public void flush() throws IOException {
writer.flush();
}
}
/**
* @return the callback
*/
public Callback getCallback() {
return callback;
}
/**
* @param callback the callback to set
*/
public void setCallback(Callback callback) {
this.callback = callback;
}
}