package org.geoserver.wfs.xslt.rest;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.converters.reflection.ReflectionConverter;
import com.thoughtworks.xstream.converters.reflection.ReflectionProvider;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.mapper.Mapper;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.StoreInfo;
import org.geoserver.catalog.WorkspaceInfo;
import org.geoserver.catalog.impl.FeatureTypeInfoImpl;
import org.geoserver.catalog.impl.ModificationProxy;
import org.geoserver.config.util.XStreamPersister;
import org.geoserver.rest.ResourceNotFoundException;
import org.geoserver.rest.RestBaseController;
import org.geoserver.rest.RestException;
import org.geoserver.rest.catalog.AbstractCatalogController;
import org.geoserver.rest.converters.XStreamMessageConverter;
import org.geoserver.rest.util.MediaTypeExtensions;
import org.geoserver.rest.wrapper.RestWrapper;
import org.geoserver.util.IOUtils;
import org.geoserver.wfs.xslt.config.TransformInfo;
import org.geoserver.wfs.xslt.config.TransformRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.util.Collection;
@RestController
@ControllerAdvice
@RequestMapping(path = RestBaseController.ROOT_PATH + "/services/wfs/transforms")
public class TransformController extends AbstractCatalogController {
@Autowired
private TransformRepository repository;
public TransformController(Catalog catalog) {
super(catalog);
}
@GetMapping(path = {"", "{transform}"}, produces = {
MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_HTML_VALUE})
public RestWrapper getTransformsInfo(
@PathVariable(name = "transform", required = false) String transformInfoName) {
if (transformInfoName == null) {
// we need to return all transforms
try {
return wrapList(repository.getAllTransforms(), TransformInfo.class);
} catch (Exception exception) {
throw new RestException(
"Error reading transforms info from repository.",
HttpStatus.INTERNAL_SERVER_ERROR, exception);
}
}
return wrapObject(getTransformInfo(transformInfoName), TransformInfo.class);
}
@GetMapping(path = "{transform}", produces = MediaTypeExtensions.APPLICATION_XSLT_VALUE)
public void getTransforms(
@PathVariable(name = "transform") String transformInfoName, OutputStream output) {
InputStream transform = getTransform(transformInfoName);
try {
IOUtils.copy(transform, output);
} catch (Exception exception) {
throw new RestException(String.format("Error writing transform '%s' XSLT.",
transformInfoName), HttpStatus.INTERNAL_SERVER_ERROR, exception);
}
}
@PostMapping(consumes = {MediaType.TEXT_XML_VALUE,
MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<String> postTransformInfo(
@RequestBody TransformInfo transformInfo, UriComponentsBuilder builder) {
validate(transformInfo);
saveTransFormInfo(transformInfo);
return buildResponse(builder, transformInfo.getName(), HttpStatus.CREATED);
}
@PostMapping(consumes = MediaTypeExtensions.APPLICATION_XSLT_VALUE)
public ResponseEntity<String> postTransform(
InputStream transform,
@RequestParam(name = "name", required = false) String transformInfoName,
@RequestParam(name = "sourceFormat", required = false) String sourceFormat,
@RequestParam(name = "outputFormat", required = false) String outputFormat,
@RequestParam(name = "outputMimeType", required = false) String outputMimeType,
@RequestParam(name = "fileExtension", required = false) String fileExtension,
UriComponentsBuilder builder) {
TransformInfo transformInfo;
try {
transformInfo = repository.getTransformInfo(transformInfoName);
} catch (Exception exception) {
throw new RestException(String.format("Error reading transform '%s' info from repository.",
transformInfoName), HttpStatus.INTERNAL_SERVER_ERROR, exception);
}
if (transformInfo == null) {
transformInfo = new TransformInfo();
transformInfo.setName(transformInfoName);
transformInfo.setSourceFormat(sourceFormat);
transformInfo.setOutputFormat(outputFormat);
transformInfo.setOutputMimeType(outputMimeType);
transformInfo.setFileExtension(fileExtension);
transformInfo.setXslt(transformInfoName + ".xslt");
validate(transformInfo);
saveTransFormInfo(transformInfo);
}
saveTransForm(transformInfo, transform);
return buildResponse(builder, transformInfo.getName(), HttpStatus.CREATED);
}
@PutMapping(path = "{transform}", consumes = {MediaType.TEXT_XML_VALUE,
MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE})
public void putTransformInfo(
@RequestBody TransformInfo transformInfo,
@PathVariable(name = "transform") String transformInfoName) {
transformInfo.setName(transformInfoName);
validate(transformInfo);
saveTransFormInfo(transformInfo);
}
@PutMapping(path = "{transform}", consumes = MediaTypeExtensions.APPLICATION_XSLT_VALUE)
public void putTransform(
InputStream transform,
@PathVariable(name = "transform") String transformInfoName) {
TransformInfo transformInfo = getTransformInfo(transformInfoName);
saveTransForm(transformInfo, transform);
}
@DeleteMapping(path = "{transform}")
public void putTransform(
@PathVariable(name = "transform") String transformInfoName) {
TransformInfo transformInfo = getTransformInfo(transformInfoName);
try {
repository.removeTransformInfo(transformInfo);
} catch (Exception exception) {
throw new RestException(String.format(
"Error deleting transformation '%s'.", transformInfoName),
HttpStatus.INTERNAL_SERVER_ERROR, exception);
}
}
private void saveTransForm(TransformInfo transformInfo, InputStream transform) {
try {
repository.putTransformSheet(transformInfo, transform);
} catch (Exception exception) {
throw new RestException(String.format("Error writing transform '%s' XSLT info to repository.",
transformInfo.getName()), HttpStatus.INTERNAL_SERVER_ERROR, exception);
}
}
private void saveTransFormInfo(TransformInfo transformInfo) {
try {
repository.putTransformInfo(transformInfo);
} catch (Exception exception) {
throw new RestException(String.format("Error writing transform '%s' info to repository.",
transformInfo.getName()), HttpStatus.INTERNAL_SERVER_ERROR, exception);
}
}
private ResponseEntity<String> buildResponse(UriComponentsBuilder builder, String transformInfoName, HttpStatus status) {
UriComponents uriComponents = builder.path(
"/services/wfs/transforms/{transform}").buildAndExpand(transformInfoName);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(uriComponents.toUri());
return new ResponseEntity<>(transformInfoName, headers, status);
}
private void validate(TransformInfo info) {
if (info.getSourceFormat() == null) {
throw new RestException(
"The transformation must have a source format", HttpStatus.BAD_REQUEST);
}
if (info.getOutputFormat() == null) {
throw new RestException(
"The transformation must have an output format", HttpStatus.BAD_REQUEST);
}
}
private TransformInfo getTransformInfo(String transformInfoName) {
TransformInfo transformInfo;
try {
transformInfo = repository.getTransformInfo(transformInfoName);
} catch (Exception exception) {
throw new RestException(String.format("Error reading transform '%s' info from repository.",
transformInfoName), HttpStatus.INTERNAL_SERVER_ERROR, exception);
}
if (transformInfo == null) {
throw new ResourceNotFoundException(String.format(
"Transform '%s' info not found.", transformInfoName));
}
return transformInfo;
}
private InputStream getTransform(String transformInfoName) {
TransformInfo transformInfo = getTransformInfo(transformInfoName);
InputStream transform;
try {
transform = repository.getTransformSheet(transformInfo);
} catch (Exception exception) {
throw new RestException(String.format(
"Error reading transform '%s' XSLT from repository.",
transformInfoName), HttpStatus.INTERNAL_SERVER_ERROR, exception);
}
if (transform == null) {
throw new ResourceNotFoundException(String.format(
"Transform '%s' XSLT not found.", transformInfoName));
}
return transform;
}
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return TransformInfo.class.isAssignableFrom(methodParameter.getParameterType());
}
@Override
public void configurePersister(XStreamPersister persister, XStreamMessageConverter converter) {
XStream xs = persister.getXStream();
xs.alias("transforms", Collection.class);
xs.alias("transform", TransformInfo.class);
xs.registerConverter(new TransformConverter(xs.getMapper(), xs.getReflectionProvider()));
xs.registerLocalConverter(TransformInfo.class, "featureType",
new FeatureTypeLinkConverter(catalog, converter));
xs.addDefaultImplementation(FeatureTypeInfoImpl.class, FeatureTypeInfo.class);
// setup security
xs.allowTypes(new Class[]{TransformInfo.class});
}
private class TransformConverter extends ReflectionConverter {
public TransformConverter(Mapper mapper, ReflectionProvider reflectionProvider) {
super(mapper, reflectionProvider);
}
@Override
public boolean canConvert(Class type) {
return TransformInfo.class.isAssignableFrom(type);
}
@Override
public void marshal(Object source, HierarchicalStreamWriter writer,
MarshallingContext context) {
TransformInfo original = (TransformInfo) source;
TransformInfo resolved = new TransformInfo(original);
FeatureTypeInfo ft = resolved.getFeatureType();
if (ft != null) {
resolved.setFeatureType(ModificationProxy.unwrap(ft));
}
super.marshal(resolved, writer, context);
}
}
private static final class FeatureTypeLinkConverter implements Converter {
private final XStreamMessageConverter converter;
private final Catalog catalog;
private FeatureTypeLinkConverter(Catalog catalog, XStreamMessageConverter converter) {
this.catalog = catalog;
this.converter = converter;
}
@Override
public boolean canConvert(Class type) {
return true;
}
@Override
public void marshal(Object source, HierarchicalStreamWriter writer,
MarshallingContext context) {
FeatureTypeInfo ft = (FeatureTypeInfo) source;
writer.startNode("name");
writer.setValue(ft.prefixedName());
writer.endNode();
StoreInfo store = ft.getStore();
WorkspaceInfo ws = store.getWorkspace();
converter.encodeLink(
"/workspaces/" + converter.encode(ws.getName()) + "/datastores/"
+ converter.encode(store.getName()) + "/featuretypes/" + converter.encode(ft.getName()),
writer);
}
@Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
String ref = null;
if (reader.hasMoreChildren()) {
while (reader.hasMoreChildren()) {
reader.moveDown();
ref = reader.getValue();
reader.moveUp();
}
} else {
ref = reader.getValue();
}
FeatureTypeInfo result = catalog.getFeatureType(ref);
if (result == null) {
result = catalog.getFeatureTypeByName(ref);
}
return result;
}
}
}