/* (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.rest.catalog;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.catalog.CascadeDeleteVisitor;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CatalogBuilder;
import org.geoserver.catalog.CatalogInfo;
import org.geoserver.catalog.DataStoreInfo;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.NamespaceInfo;
import org.geoserver.catalog.WorkspaceInfo;
import org.geoserver.config.util.XStreamPersister;
import org.geoserver.rest.ObjectToMapWrapper;
import org.geoserver.rest.ResourceNotFoundException;
import org.geoserver.rest.RestBaseController;
import org.geoserver.rest.RestException;
import org.geoserver.rest.converters.XStreamMessageConverter;
import org.geoserver.rest.util.MediaTypeExtensions;
import org.geoserver.rest.wrapper.RestWrapper;
import org.geotools.data.DataAccessFactory;
import org.geotools.util.logging.Logging;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.vfny.geoserver.util.DataStoreUtils;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import freemarker.template.ObjectWrapper;
import freemarker.template.SimpleHash;
import freemarker.template.TemplateModelException;
@RestController
@ControllerAdvice
@RequestMapping(path = RestBaseController.ROOT_PATH + "/workspaces/{workspaceName}/datastores")
public class DataStoreController extends AbstractCatalogController {
private static final Logger LOGGER = Logging.getLogger(DataStoreController.class);
@Autowired
public DataStoreController(@Qualifier("catalog") Catalog catalog) {
super(catalog);
}
/*
Get Mappings
*/
@GetMapping(produces = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE,
MediaType.TEXT_HTML_VALUE })
public RestWrapper<DataStoreInfo> dataStoresGet(@PathVariable String workspaceName) {
WorkspaceInfo ws = catalog.getWorkspaceByName(workspaceName);
if(ws == null) {
throw new ResourceNotFoundException("No such workspace : " + workspaceName);
}
List<DataStoreInfo> dataStores = catalog
.getDataStoresByWorkspace(ws);
return wrapList(dataStores, DataStoreInfo.class);
}
@GetMapping(path = "{storeName}", produces = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE,
MediaType.TEXT_HTML_VALUE })
public RestWrapper<DataStoreInfo> dataStoreGet(
@PathVariable String workspaceName,
@PathVariable String storeName) {
DataStoreInfo dataStore = getExistingDataStore(workspaceName, storeName);
return wrapObject(dataStore, DataStoreInfo.class);
}
@PostMapping(consumes = {
MediaType.APPLICATION_JSON_VALUE,
MediaTypeExtensions.TEXT_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE,
MediaType.TEXT_XML_VALUE })
public ResponseEntity<String> dataStorePost(
@RequestBody DataStoreInfo dataStore,
@PathVariable String workspaceName,
UriComponentsBuilder builder) {
if ( dataStore.getWorkspace() != null ) {
//ensure the specifried workspace matches the one dictated by the uri
WorkspaceInfo ws = dataStore.getWorkspace();
if ( !workspaceName.equals( ws.getName() ) ) {
throw new RestException( "Expected workspace " + workspaceName +
" but client specified " + ws.getName(), HttpStatus.FORBIDDEN );
}
} else {
dataStore.setWorkspace( catalog.getWorkspaceByName(workspaceName) );
}
dataStore.setEnabled(true);
//if no namespace parameter set, set it
//TODO: we should really move this sort of thing to be something central
if (!dataStore.getConnectionParameters().containsKey("namespace")) {
WorkspaceInfo ws = dataStore.getWorkspace();
NamespaceInfo ns = catalog.getNamespaceByPrefix(ws.getName());
if (ns == null) {
ns = catalog.getDefaultNamespace();
}
if (ns != null) {
dataStore.getConnectionParameters().put("namespace", ns.getURI());
}
}
//attempt to set the datastore type
try {
DataAccessFactory factory =
DataStoreUtils.aquireFactory(dataStore.getConnectionParameters());
dataStore.setType(factory.getDisplayName());
}
catch(Exception e) {
LOGGER.warning("Unable to determine datastore type from connection parameters");
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "", e);
}
}
catalog.validate(dataStore, true).throwIfInvalid();
catalog.add(dataStore);
String storeName = dataStore.getName();
LOGGER.info("POST data store " + storeName);
UriComponents uriComponents = builder.path("/workspaces/{workspaceName}/datastores/{storeName}")
.buildAndExpand(workspaceName, storeName);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(uriComponents.toUri());
return new ResponseEntity<>(storeName, headers, HttpStatus.CREATED);
}
@PutMapping(value = "{storeName}", consumes = {
MediaType.APPLICATION_JSON_VALUE,
MediaTypeExtensions.TEXT_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE,
MediaType.TEXT_XML_VALUE })
public void dataStorePut(
@RequestBody DataStoreInfo info,
@PathVariable String workspaceName,
@PathVariable String storeName) {
DataStoreInfo original = getExistingDataStore(workspaceName, storeName);
if (!original.getName().equalsIgnoreCase(info.getName())) {
throw new RestException("can not change name of a datastore", HttpStatus.FORBIDDEN);
}
if (!original.getWorkspace().getName().equalsIgnoreCase(info.getWorkspace().getName())) {
throw new RestException("can not change name of a workspace", HttpStatus.FORBIDDEN);
}
new CatalogBuilder(catalog).updateDataStore(original, info);
catalog.validate(original, false).throwIfInvalid();
catalog.save(original);
clear(original);
LOGGER.info("PUT datastore " + workspaceName + "," + storeName);
}
@DeleteMapping(value = "{storeName}")
public void dataStoreDelete(
@PathVariable String workspaceName,
@PathVariable String storeName,
@RequestParam(name = "recurse", required = false, defaultValue = "false") boolean recurse,
@RequestParam(name = "purge", required = false, defaultValue = "none") String deleteType) throws IOException {
DataStoreInfo ds = getExistingDataStore(workspaceName, storeName);
if (!recurse) {
if (!catalog.getStoresByWorkspace(workspaceName, DataStoreInfo.class).isEmpty()) {
for (DataStoreInfo dataStoreInfo : catalog.getStoresByWorkspace(workspaceName, DataStoreInfo.class)) {
if (dataStoreInfo.getName().equalsIgnoreCase(storeName)){
break;
}
throw new RestException("datastore not empty", HttpStatus.FORBIDDEN);
}
}
catalog.remove(ds);
} else {
new CascadeDeleteVisitor(catalog).visit(ds);
}
catalog.remove(ds);
clear(ds);
LOGGER.info("DELETE datastore " + workspaceName + ":s" + workspaceName);
}
private DataStoreInfo getExistingDataStore(String workspaceName, String storeName) {
DataStoreInfo original = catalog.getDataStoreByName(workspaceName, storeName);
if(original == null) {
throw new ResourceNotFoundException(
"No such datastore: " + workspaceName + "," + storeName);
}
return original;
}
void clear(DataStoreInfo info) {
catalog.getResourcePool().clear(info);
}
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return DataStoreInfo.class.isAssignableFrom(methodParameter.getParameterType());
}
@Override
public void configurePersister(XStreamPersister persister, XStreamMessageConverter converter) {
persister.setCallback(new XStreamPersister.Callback() {
@Override
protected Class<DataStoreInfo> getObjectClass() {
return DataStoreInfo.class;
}
@Override
protected CatalogInfo getCatalogObject() {
Map<String, String> uriTemplateVars = (Map<String, String>) RequestContextHolder.getRequestAttributes().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
String workspace = uriTemplateVars.get("workspaceName");
String datastore = uriTemplateVars.get("storeName");
if (workspace == null || datastore == null) {
return null;
}
return catalog.getDataStoreByName(workspace, datastore);
}
@Override
protected void postEncodeDataStore(DataStoreInfo ds,
HierarchicalStreamWriter writer, MarshallingContext context) {
// add a link to the featuretypes
writer.startNode("featureTypes");
converter.encodeCollectionLink("featuretypes", writer);
writer.endNode();
}
@Override
protected void postEncodeReference(Object obj, String ref, String prefix,
HierarchicalStreamWriter writer, MarshallingContext context) {
if (obj instanceof WorkspaceInfo) {
converter.encodeLink("/workspaces/" + converter.encode(ref), writer);
}
}
});
}
@Override
protected String getTemplateName(Object object) {
if (object instanceof DataStoreInfo) {
return "DataStoreInfo";
}
return null;
}
@Override
protected <T> ObjectWrapper createObjectWrapper(Class<T> clazz) {
return new ObjectToMapWrapper<DataStoreInfo>(DataStoreInfo.class) {
@Override
protected void wrapInternal(Map properties, SimpleHash model, DataStoreInfo dataStoreInfo) {
if (properties == null) {
try {
properties = model.toMap();
} catch (TemplateModelException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
List<Map<String, Map<String, String>>> dsProps = new ArrayList<>();
List<FeatureTypeInfo> featureTypes = catalog.getFeatureTypesByDataStore(dataStoreInfo);
for (FeatureTypeInfo ft : featureTypes){
Map<String, String> names = new HashMap<>();
names.put("name", ft.getName());
dsProps.add(Collections.singletonMap("properties", names));
}
if (!dsProps.isEmpty())
properties.putIfAbsent("featureTypes", dsProps);
}
@Override
protected void wrapInternal(SimpleHash model, @SuppressWarnings("rawtypes") Collection object) {
for (Object w : object) {
DataStoreInfo wk = (DataStoreInfo) w;
wrapInternal(null, model, wk);
}
}
};
}
}