/* (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 com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CatalogBuilder;
import org.geoserver.catalog.CatalogInfo;
import org.geoserver.catalog.CoverageInfo;
import org.geoserver.catalog.CoverageStoreInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.NamespaceInfo;
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.converters.XStreamMessageConverter;
import org.geoserver.rest.util.MediaTypeExtensions;
import org.geoserver.rest.wrapper.RestWrapper;
import org.geotools.util.logging.Logging;
import org.opengis.coverage.grid.GridCoverageReader;
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 java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Logger;
import java.util.stream.Collectors;
@RestController
@ControllerAdvice
@RequestMapping(path = RestBaseController.ROOT_PATH + "/workspaces/{workspaceName}")
public class CoverageController extends AbstractCatalogController {
private static final Logger LOGGER = Logging.getLogger(CoverageController.class);
@Autowired
public CoverageController(@Qualifier("catalog") Catalog catalog) {
super(catalog);
}
@GetMapping(path = "coveragestores/{storeName}/coverages", produces = {
MediaType.TEXT_XML_VALUE,
MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE,
MediaType.TEXT_HTML_VALUE})
public Object coveragesGet(
@RequestParam(name = "list", required = false) String list,
@PathVariable String workspaceName,
@PathVariable String storeName) {
// find the coverage store
CoverageStoreInfo coverageStore = getExistingCoverageStore(workspaceName, storeName);
if (list != null && list.equalsIgnoreCase("all")) {
// we need to ask the coverage reader which coverages are available
List<String> coverages = getStoreCoverages(coverageStore);
return new StringsList(coverages, "coverageName");
}
// get the store configured coverages
List<CoverageInfo> coverages = catalog.getCoveragesByCoverageStore(coverageStore);
return wrapList(coverages, CoverageInfo.class);
}
@GetMapping(path = "coverages", produces = {
MediaType.TEXT_XML_VALUE,
MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE,
MediaType.TEXT_HTML_VALUE,
MediaTypeExtensions.TEXT_JSON_VALUE})
public Object coveragesGet(
@RequestParam(name = "list", required = false) String list,
@PathVariable String workspaceName) {
// get the workspace name space
NamespaceInfo nameSpace = catalog.getNamespaceByPrefix(workspaceName);
if (nameSpace == null) {
// could not find the namespace associated with the desired workspace
throw new ResourceNotFoundException(String.format(
"Name space not found for workspace '%s'.", workspaceName));
}
if (list != null && list.equalsIgnoreCase("all")) {
// we need to ask the coverage reader of each available coverage store which coverages are available
List<String> coverages = catalog.getCoverageStores().stream()
.flatMap(store -> getStoreCoverages(store).stream()).collect(Collectors.toList());
return new StringsList(coverages, "coverageName");
}
// get all the coverages of the workspace \ name space
List<CoverageInfo> coverages = catalog.getCoveragesByNamespace(nameSpace);
return wrapList(coverages, CoverageInfo.class);
}
@GetMapping(path = "coveragestores/{storeName}/coverages/{coverageName}", produces = {
MediaType.TEXT_XML_VALUE,
MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE,
MediaType.TEXT_HTML_VALUE,
MediaTypeExtensions.TEXT_JSON_VALUE})
public RestWrapper<CoverageInfo> coverageGet(
@PathVariable String workspaceName,
@PathVariable String storeName,
@PathVariable String coverageName) {
CoverageStoreInfo coverageStore = getExistingCoverageStore(workspaceName, storeName);
List<CoverageInfo> coverages = catalog.getCoveragesByCoverageStore(coverageStore);
Optional<CoverageInfo> optCoverage = coverages.stream()
.filter(ci -> coverageName.equals(ci.getName())).findFirst();
if (!optCoverage.isPresent()) {
throw new ResourceNotFoundException(String.format(
"No such coverage: %s,%s,%s", workspaceName, storeName, coverageName));
}
CoverageInfo coverage = optCoverage.get();
checkCoverageExists(coverage, workspaceName, coverageName);
return wrapObject(coverage, CoverageInfo.class);
}
@GetMapping(path = "coverages/{coverageName}", produces = {
MediaType.TEXT_XML_VALUE,
MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE,
MediaType.TEXT_HTML_VALUE,
MediaTypeExtensions.TEXT_JSON_VALUE})
public RestWrapper<CoverageInfo> coverageGet(
@PathVariable String workspaceName,
@PathVariable String coverageName) {
// get the workspace name space
NamespaceInfo nameSpace = catalog.getNamespaceByPrefix(workspaceName);
if (nameSpace == null) {
// could not find the namespace associated with the desired workspace
throw new ResourceNotFoundException(String.format(
"Name space not found for workspace '%s'.", workspaceName));
}
CoverageInfo coverage = catalog.getCoverageByName(nameSpace, coverageName);
checkCoverageExists(coverage, workspaceName, coverageName);
return wrapObject(coverage, CoverageInfo.class);
}
@PostMapping(path = {"coverages", "coveragestores/{storeName}/coverages"}, consumes = {
MediaType.TEXT_XML_VALUE,
MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<String> coveragePost(
@RequestBody CoverageInfo coverage,
@PathVariable String workspaceName,
@PathVariable(required = false) String storeName,
UriComponentsBuilder builder) throws Exception {
String coverageName = handleObjectPost(coverage, workspaceName, storeName);
UriComponents uriComponents;
if (storeName == null) {
uriComponents = builder.path("/workspaces/{workspaceName}/coverages/{coverageName}")
.buildAndExpand(workspaceName, storeName, coverageName);
} else {
uriComponents = builder.path("/workspaces/{workspaceName}/coveragestores/{storeName}/coverages/{coverageName}")
.buildAndExpand(workspaceName, storeName, coverageName);
}
HttpHeaders headers = new HttpHeaders();
headers.setLocation(uriComponents.toUri());
return new ResponseEntity<>(coverageName, headers, HttpStatus.CREATED);
}
@PutMapping(path = "coveragestores/{storeName}/coverages/{coverageName}", consumes = {
MediaType.TEXT_XML_VALUE,
MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE})
public void coveragePut(
@RequestBody CoverageInfo coverage,
@PathVariable String workspaceName,
@PathVariable String storeName,
@PathVariable String coverageName,
@RequestParam(required = false) String calculate) throws Exception {
CoverageStoreInfo cs = catalog.getCoverageStoreByName(workspaceName, storeName);
CoverageInfo original = catalog.getCoverageByCoverageStore(cs, coverageName);
checkCoverageExists(original, workspaceName, coverageName);
calculateOptionalFields(coverage, original, calculate);
new CatalogBuilder(catalog).updateCoverage(original, coverage);
catalog.validate(original, false).throwIfInvalid();
catalog.save(original);
catalog.getResourcePool().clear(original.getStore());
LOGGER.info("PUT coverage " + storeName + "," + coverage);
}
@DeleteMapping(path = "coveragestores/{storeName}/coverages/{coverageName}")
protected void coverageDelete(
@PathVariable String workspaceName,
@PathVariable String storeName,
@PathVariable String coverageName,
@RequestParam(name = "recurse", defaultValue = "false") boolean recurse) {
CoverageStoreInfo ds = catalog.getCoverageStoreByName(workspaceName, storeName);
CoverageInfo c = catalog.getCoverageByCoverageStore(ds, coverageName);
if (c == null) {
throw new RestException(String.format(
"Coverage '%s' not found.", coverageName), HttpStatus.NOT_FOUND);
}
List<LayerInfo> layers = catalog.getLayers(c);
if (recurse) {
//by recurse we clear out all the layers that public this resource
for (LayerInfo l : layers) {
catalog.remove(l);
LOGGER.info("DELETE layer " + l.getName());
}
} else {
if (!layers.isEmpty()) {
throw new RestException("coverage referenced by layer(s)", HttpStatus.FORBIDDEN);
}
}
catalog.remove(c);
catalog.getResourcePool().clear(c.getStore());
LOGGER.info("DELETE coverage " + storeName + "," + coverageName);
}
private List<String> getStoreCoverages(CoverageStoreInfo coverageStore) {
try {
GridCoverageReader reader = coverageStore.getGridCoverageReader(null, null);
return Arrays.stream(reader.getGridCoverageNames()).collect(Collectors.toList());
} catch (Exception exception) {
// the read failed to retrieve the available coverages for publishing
throw new RuntimeException(
"Error getting coverages from coverage reader.", exception);
}
}
/**
* If the coverage doesn't exists throws a REST exception with HTTP 404 code.
*/
private void checkCoverageExists(CoverageInfo coverage, String workspaceName, String coverageName) {
if (coverage == null) {
throw new ResourceNotFoundException(String.format(
"No such coverage: %s,%s", workspaceName, coverageName));
}
}
/**
* Helper method that find a store based on the workspace name and store name.
*/
private CoverageStoreInfo getExistingCoverageStore(String workspaceName, String storeName) {
CoverageStoreInfo original = catalog.getCoverageStoreByName(workspaceName, storeName);
if (original == null) {
throw new ResourceNotFoundException(
"No such coverage store: " + workspaceName + "," + storeName);
}
return original;
}
/**
* Helper method that handles the POST of a coverage. This handles both the cases
* when the store is provided and when the store is not provided.
*/
private String handleObjectPost(CoverageInfo coverage, String workspace, String coverageStoreName) throws Exception {
if (coverage.getStore() == null) {
CoverageStoreInfo ds = catalog.getCoverageStoreByName(workspace, coverageStoreName);
coverage.setStore(ds);
}
final boolean isNew = isNewCoverage(coverage);
String nativeCoverageName = coverage.getNativeCoverageName();
if (nativeCoverageName == null) {
nativeCoverageName = coverage.getNativeName();
}
CatalogBuilder builder = new CatalogBuilder(catalog);
CoverageStoreInfo store = coverage.getStore();
builder.setStore(store);
// We handle 2 different cases here
if (!isNew) {
// Configuring a partially defined coverage
builder.initCoverage(coverage, nativeCoverageName);
} else {
// Configuring a brand new coverage (only name has been specified)
String specifiedName = coverage.getName();
coverage = builder.buildCoverageByName(nativeCoverageName, specifiedName);
}
NamespaceInfo ns = coverage.getNamespace();
if (ns != null && !ns.getPrefix().equals(workspace)) {
//TODO: change this once the two can be different and we untie namespace
// from workspace
LOGGER.warning("Namespace: " + ns.getPrefix() + " does not match workspace: " + workspace + ", overriding.");
ns = null;
}
if (ns == null) {
//infer from workspace
ns = catalog.getNamespaceByPrefix(workspace);
coverage.setNamespace(ns);
}
coverage.setEnabled(true);
catalog.validate(coverage, true).throwIfInvalid();
catalog.add(coverage);
//create a layer for the coverage
catalog.add(builder.buildLayer(coverage));
LOGGER.info("POST coverage " + coverageStoreName + "," + coverage.getName());
return coverage.getName();
}
/**
* This method returns {@code true} in case we have POSTed a Coverage object with the name only, as an instance
* when configuring a new coverage which has just been harvested.
*
* @param coverage
*/
private boolean isNewCoverage(CoverageInfo coverage) {
return coverage.getName() != null && (coverage.isAdvertised()) && (!coverage.isEnabled())
&& (coverage.getAlias() == null) && (coverage.getCRS() == null)
&& (coverage.getDefaultInterpolationMethod() == null)
&& (coverage.getDescription() == null) && (coverage.getDimensions() == null)
&& (coverage.getGrid() == null) && (coverage.getInterpolationMethods() == null)
&& (coverage.getKeywords() == null) && (coverage.getLatLonBoundingBox() == null)
&& (coverage.getMetadata() == null) && (coverage.getNativeBoundingBox() == null)
&& (coverage.getNativeCRS() == null) && (coverage.getNativeFormat() == null)
&& (coverage.getProjectionPolicy() == null) && (coverage.getSRS() == null)
&& (coverage.getResponseSRS() == null) && (coverage.getRequestSRS() == null);
}
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return CoverageInfo.class.isAssignableFrom(methodParameter.getParameterType());
}
@Override
public void configurePersister(XStreamPersister persister, XStreamMessageConverter converter) {
persister.setCallback(new XStreamPersister.Callback() {
@Override
protected Class<CoverageInfo> getObjectClass() {
return CoverageInfo.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 coveragestore = uriTemplateVars.get("storeName");
String coverage = uriTemplateVars.get("coverageName");
if (workspace == null || coveragestore == null || coverage == null) {
return null;
}
CoverageStoreInfo cs = catalog.getCoverageStoreByName(workspace, coveragestore);
if (cs == null) {
return null;
}
return catalog.getCoverageByCoverageStore(cs, coverage);
}
@Override
protected void postEncodeReference(Object obj, String ref, String prefix,
HierarchicalStreamWriter writer, MarshallingContext context) {
if (obj instanceof NamespaceInfo) {
NamespaceInfo ns = (NamespaceInfo) obj;
converter.encodeLink("/namespaces/" + converter.encode(ns.getPrefix()), writer);
}
if (obj instanceof CoverageStoreInfo) {
CoverageStoreInfo cs = (CoverageStoreInfo) obj;
converter.encodeLink("/workspaces/" + converter.encode(cs.getWorkspace().getName()) +
"/coveragestores/" + converter.encode(cs.getName()), writer);
}
}
});
}
}