/* (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.io.Serializable; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import freemarker.template.ObjectWrapper; import org.geoserver.catalog.*; 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.DataAccess; import org.geotools.data.DataStore; import org.geotools.data.FeatureSource; import org.geotools.feature.NameImpl; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.util.logging.Logging; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.FeatureType; import org.opengis.feature.type.Name; 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.context.request.ServletRequestAttributes; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.util.UriComponentsBuilder; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import freemarker.template.SimpleHash; /** * Feature type controller */ @RestController @ControllerAdvice @RequestMapping(path = { RestBaseController.ROOT_PATH + "/workspaces/{workspaceName}/featuretypes", RestBaseController.ROOT_PATH + "/workspaces/{workspaceName}/datastores/{dataStoreName}/featuretypes"}) public class FeatureTypeController extends AbstractCatalogController { private static final Logger LOGGER = Logging.getLogger(CoverageStoreController.class); @Autowired public FeatureTypeController(@Qualifier("catalog") Catalog catalog) { super(catalog); } @GetMapping(produces = { MediaType.TEXT_HTML_VALUE, MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE }) public Object featureTypesGet( @PathVariable String workspaceName, @PathVariable(required = false) String dataStoreName, @RequestParam(defaultValue = "configured") String list) { if ("available".equalsIgnoreCase(list) || "available_with_geom".equalsIgnoreCase(list) || "all".equalsIgnoreCase(list)) { DataStoreInfo info = getExistingDataStore(workspaceName, dataStoreName); // flag to control whether to filter out types without geometry boolean skipNoGeom = "available_with_geom".equalsIgnoreCase(list); // list of available feature types List<String> featureTypes = new ArrayList<>(); try { DataStore ds = (DataStore) info.getDataStore(null); String[] featureTypeNames = ds.getTypeNames(); for (String featureTypeName : featureTypeNames) { FeatureTypeInfo ftinfo = catalog.getFeatureTypeByDataStore(info, featureTypeName); if (ftinfo == null) { // The feature type is not in catalog, so add it to the return list. // check whether to filter by geometry if (skipNoGeom) { try { FeatureType featureType = ds.getSchema(featureTypeName); if (featureType.getGeometryDescriptor() == null) { // skip continue; } } catch (IOException e) { LOGGER.log(Level.WARNING, "Unable to load schema for feature type " + featureTypeName, e); } } featureTypes.add(featureTypeName); } else if ("all".equalsIgnoreCase(list)) { // The feature type is already configured, but "all" was specified, so add it to the return list. featureTypes.add(featureTypeName); } } } catch (IOException e) { throw new ResourceNotFoundException("Could not load datastore: " + dataStoreName); } return new StringsList(featureTypes, "featureTypeName"); } else { List<FeatureTypeInfo> fts; if (dataStoreName != null) { DataStoreInfo dataStore = catalog.getDataStoreByName(workspaceName, dataStoreName); fts = catalog.getFeatureTypesByDataStore(dataStore); } else { NamespaceInfo ns = catalog.getNamespaceByPrefix(workspaceName); fts = catalog.getFeatureTypesByNamespace(ns); } return wrapList(fts, FeatureTypeInfo.class); } } @PostMapping(consumes = { MediaTypeExtensions.TEXT_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_XML_VALUE }) public ResponseEntity featureTypePost( @PathVariable String workspaceName, @PathVariable(required = false) String dataStoreName, @RequestBody FeatureTypeInfo ftInfo, UriComponentsBuilder builder) throws Exception { DataStoreInfo dsInfo = getExistingDataStore(workspaceName, dataStoreName); // ensure the store matches up if (ftInfo.getStore() != null) { if (!dataStoreName.equals(ftInfo.getStore().getName())) { throw new RestException("Expected datastore " + dataStoreName + " but client specified " + ftInfo.getStore().getName(), HttpStatus.FORBIDDEN); } } else { ftInfo.setStore(dsInfo); } // ensure workspace/namespace matches up if (ftInfo.getNamespace() != null) { if (!workspaceName.equals(ftInfo.getNamespace().getPrefix())) { throw new RestException("Expected workspace " + workspaceName + " but client specified " + ftInfo.getNamespace().getPrefix(), HttpStatus.FORBIDDEN); } } else { ftInfo.setNamespace(catalog.getNamespaceByPrefix(workspaceName)); } ftInfo.setEnabled(true); // now, does the feature type exist? If not, create it DataAccess dataAccess = dsInfo.getDataStore(null); if (dataAccess instanceof DataStore) { String typeName = ftInfo.getName(); if (ftInfo.getNativeName() != null) { typeName = ftInfo.getNativeName(); } boolean typeExists = false; DataStore dataStore = (DataStore) dataAccess; for (String name : dataStore.getTypeNames()) { if (name.equals(typeName)) { typeExists = true; break; } } // check to see if this is a virtual JDBC feature type MetadataMap mdm = ftInfo.getMetadata(); boolean virtual = mdm != null && mdm.containsKey(FeatureTypeInfo.JDBC_VIRTUAL_TABLE); if (!virtual && !typeExists) { dataStore.createSchema(buildFeatureType(ftInfo)); // the attributes created might not match up 1-1 with the actual spec due to // limitations of the data store, have it re-compute them ftInfo.getAttributes().clear(); List<String> typeNames = Arrays.asList(dataStore.getTypeNames()); // handle Oracle oddities // TODO: use the incoming store capabilites API to better handle the name transformation if (!typeNames.contains(typeName) && typeNames.contains(typeName.toUpperCase())) { ftInfo.setNativeName(ftInfo.getName().toLowerCase()); } } } CatalogBuilder cb = new CatalogBuilder(catalog); cb.initFeatureType(ftInfo); // attempt to fill in metadata from underlying feature source try { FeatureSource featureSource = dataAccess .getFeatureSource(new NameImpl(ftInfo.getNativeName())); if (featureSource != null) { cb.setupMetadata(ftInfo, featureSource); } } catch (Exception e) { LOGGER.log(Level.WARNING, "Unable to fill in metadata from underlying feature source", e); } if (ftInfo.getStore() == null) { // get from requests ftInfo.setStore(dsInfo); } NamespaceInfo ns = ftInfo.getNamespace(); if (ns != null && !ns.getPrefix().equals(workspaceName)) { // TODO: change this once the two can be different and we untie namespace // from workspace LOGGER.warning("Namespace: " + ns.getPrefix() + " does not match workspace: " + workspaceName + ", overriding."); ns = null; } if (ns == null) { // infer from workspace ns = catalog.getNamespaceByPrefix(workspaceName); ftInfo.setNamespace(ns); } ftInfo.setEnabled(true); catalog.validate(ftInfo, true).throwIfInvalid(); catalog.add(ftInfo); // create a layer for the feature type catalog.add(new CatalogBuilder(catalog).buildLayer(ftInfo)); LOGGER.info("POST feature type" + dataStoreName + "," + ftInfo.getName()); HttpHeaders headers = new HttpHeaders(); headers.setLocation( builder.path("/workspaces/{workspaceName}/datastores/{datastoreName}/featuretypes/" + ftInfo.getName()).buildAndExpand(workspaceName, dataStoreName).toUri()); return new ResponseEntity<>("", headers, HttpStatus.CREATED); } @GetMapping(path = "/{featureTypeName}", produces = { MediaType.TEXT_HTML_VALUE, MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE }) public RestWrapper featureTypeGet( @PathVariable String workspaceName, @PathVariable(required = false) String dataStoreName, @PathVariable String featureTypeName, @RequestParam(name = "quietOnNotFound", required = false, defaultValue = "false") Boolean quietOnNotFound) { DataStoreInfo dsInfo = getExistingDataStore(workspaceName, dataStoreName); FeatureTypeInfo ftInfo = catalog.getFeatureTypeByDataStore(dsInfo, featureTypeName); checkFeatureTypeExists(ftInfo, workspaceName, dataStoreName, featureTypeName); return wrapObject(ftInfo, FeatureTypeInfo.class, "No such feature type: "+featureTypeName, quietOnNotFound); } @PutMapping(path = "/{featureTypeName}", produces = { MediaType.TEXT_HTML_VALUE, MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE }) public void featureTypePut( @PathVariable String workspaceName, @PathVariable(required = false) String dataStoreName, @PathVariable String featureTypeName, @RequestBody FeatureTypeInfo featureTypeUpdate, @RequestParam(name = "recalculate", required = false) String recalculate) { DataStoreInfo dsInfo = getExistingDataStore(workspaceName, dataStoreName); FeatureTypeInfo ftInfo = catalog.getFeatureTypeByDataStore(dsInfo, featureTypeName); checkFeatureTypeExists(ftInfo, workspaceName, dataStoreName, featureTypeName); Map<String, Serializable> parametersCheck = ftInfo.getStore() .getConnectionParameters(); calculateOptionalFields(featureTypeUpdate, ftInfo, recalculate); CatalogBuilder helper = new CatalogBuilder(catalog); helper.updateFeatureType(ftInfo, featureTypeUpdate); catalog.validate(ftInfo, false).throwIfInvalid(); catalog.save(ftInfo); catalog.getResourcePool().clear(ftInfo); Map<String, Serializable> parameters = ftInfo.getStore().getConnectionParameters(); MetadataMap mdm = ftInfo.getMetadata(); boolean virtual = mdm != null && mdm.containsKey(FeatureTypeInfo.JDBC_VIRTUAL_TABLE); if (!virtual && parameters.equals(parametersCheck)) { LOGGER.info("PUT FeatureType" + dataStoreName + "," + featureTypeName + " updated metadata only"); } else { LOGGER.info("PUT featureType" + dataStoreName + "," + featureTypeName + " updated metadata and data access"); catalog.getResourcePool().clear(ftInfo.getStore()); } } @DeleteMapping(path = "{featureTypeName}", produces = { MediaType.TEXT_HTML_VALUE, MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE }) public void featureTypeDelete( @PathVariable String workspaceName, @PathVariable(required = false) String dataStoreName, @PathVariable String featureTypeName, @RequestParam(name = "recurse", defaultValue = "false") Boolean recurse) { DataStoreInfo dsInfo = getExistingDataStore(workspaceName, dataStoreName); FeatureTypeInfo ftInfo = catalog.getFeatureTypeByDataStore(dsInfo, featureTypeName); checkFeatureTypeExists(ftInfo, workspaceName, dataStoreName, featureTypeName); List<LayerInfo> layers = catalog.getLayers(ftInfo); 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("feature type referenced by layer(s)", HttpStatus.FORBIDDEN); } } catalog.remove(ftInfo); LOGGER.info("DELETE feature type" + dataStoreName + "," + featureTypeName); } /** * If the feature type doesn't exists throws a REST exception with HTTP 404 code. */ private void checkFeatureTypeExists(FeatureTypeInfo featureType, String workspaceName, String dataStoreName, String featureTypeName) { if (featureType == null && dataStoreName == null) { throw new ResourceNotFoundException(String.format( "No such feature type: %s,%s", workspaceName, featureTypeName)); } else if (featureType == null) { throw new ResourceNotFoundException(String.format( "No such feature type: %s,%s,%s", workspaceName, dataStoreName, featureTypeName)); } } /** * Helper method that find a store based on the workspace name and store name. */ private DataStoreInfo getExistingDataStore(String workspaceName, String storeName) { DataStoreInfo original = catalog.getDataStoreByName(workspaceName, storeName); if (original == null) { throw new ResourceNotFoundException( "No such data store: " + workspaceName + "," + storeName); } return original; } SimpleFeatureType buildFeatureType(FeatureTypeInfo fti) { // basic checks if (fti.getName() == null) { throw new RestException("Trying to create new feature type inside the store, " + "but no feature type name was specified", HttpStatus.BAD_REQUEST); } else if (fti.getAttributes() == null || fti.getAttributes() == null) { throw new RestException("Trying to create new feature type inside the store, " + "but no attributes were specified", HttpStatus.BAD_REQUEST); } SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder(); if (fti.getNativeName() != null) { builder.setName(fti.getNativeName()); } else { builder.setName(fti.getName()); } if (fti.getNativeCRS() != null) { builder.setCRS(fti.getNativeCRS()); } else if (fti.getCRS() != null) { builder.setCRS(fti.getCRS()); } else if (fti.getSRS() != null) { builder.setSRS(fti.getSRS()); } for (AttributeTypeInfo ati : fti.getAttributes()) { if (ati.getLength() != null && ati.getLength() > 0) { builder.length(ati.getLength()); } builder.nillable(ati.isNillable()); builder.add(ati.getName(), ati.getBinding()); } return builder.buildFeatureType(); } @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return FeatureTypeInfo.class.isAssignableFrom(methodParameter.getParameterType()); } @Override public void configurePersister(XStreamPersister persister, XStreamMessageConverter converter) { ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder .getRequestAttributes(); String method = attrs.getRequest().getMethod(); if ("GET".equalsIgnoreCase(method)) { persister.setHideFeatureTypeAttributes(); } persister.setCallback(new XStreamPersister.Callback() { @Override protected Class<FeatureTypeInfo> getObjectClass() { return FeatureTypeInfo.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 featuretype = uriTemplateVars.get("featureTypeName"); String datastore = uriTemplateVars.get("dataStoreName"); if (workspace == null || datastore == null || featuretype == null) { return null; } DataStoreInfo ds = catalog.getDataStoreByName(workspace, datastore); if (ds == null) { return null; } return catalog.getFeatureTypeByDataStore(ds, featuretype); } @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 DataStoreInfo) { DataStoreInfo ds = (DataStoreInfo) obj; converter .encodeLink( "/workspaces/" + converter.encode(ds.getWorkspace().getName()) + "/datastores/" + converter.encode(ds.getName()), writer); } } @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); } } }); } @Override protected <T> ObjectWrapper createObjectWrapper(Class<T> clazz) { return new ObjectToMapWrapper<FeatureTypeInfo>(FeatureTypeInfo.class) { @Override protected void wrapInternal(Map properties, SimpleHash model, FeatureTypeInfo object) { try { properties.put("boundingBox", object.boundingBox()); } catch (Exception e) { throw new RuntimeException(e); } } }; } }