/* (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.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.logging.Logger; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.CatalogBuilder; import org.geoserver.catalog.CatalogInfo; import org.geoserver.catalog.LayerInfo; import org.geoserver.catalog.NamespaceInfo; import org.geoserver.catalog.Predicates; import org.geoserver.catalog.WMSLayerInfo; import org.geoserver.catalog.WMSStoreInfo; import org.geoserver.catalog.util.CloseableIterator; 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.ows.Layer; import org.geotools.data.wms.WebMapServer; import org.geotools.util.logging.Logging; import org.opengis.filter.Filter; 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 com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import freemarker.template.ObjectWrapper; import freemarker.template.SimpleHash; /** * Example style resource controller */ @RestController @ControllerAdvice @RequestMapping(path = {RestBaseController.ROOT_PATH+"/workspaces/{workspaceName}/wmslayers", RestBaseController.ROOT_PATH+"/workspaces/{workspaceName}/wmsstores/{storeName}/wmslayers"}) public class WMSLayerController extends AbstractCatalogController { private static final Logger LOGGER = Logging.getLogger(WMSLayerController.class); @Autowired public WMSLayerController(@Qualifier("catalog") Catalog catalog) { super(catalog); } @GetMapping(produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_HTML_VALUE}) public Object layersGet( @PathVariable String workspaceName, @PathVariable(required = false) String storeName, @RequestParam(required = false, defaultValue = "false") boolean quietOnNotFound, @RequestParam(required = false, defaultValue = "configured") String list) { switch(list) { case "available": LOGGER.fine(()->logMessage("GET available WMS layers from ", workspaceName, storeName, null)); return new AvailableResources(getAvailableLayersInternal(workspaceName, storeName, quietOnNotFound), "wmsLayerName"); case "configured": LOGGER.fine(()->logMessage("GET configured WMS layers from ", workspaceName, storeName, null)); return wrapList(getConfiguredLayersInternal(workspaceName, storeName, quietOnNotFound), WMSLayerInfo.class); default: throw new RestException("Unknown list type "+list, HttpStatus.NOT_IMPLEMENTED); } } Collection<WMSStoreInfo> getStoresInternal(NamespaceInfo ns, String storeName, boolean quietOnNotFound) { if(Objects.nonNull(storeName)) { return Collections.singleton(getStoreInternal(ns, storeName)); } else { return catalog.getStoresByWorkspace(ns.getPrefix(), WMSStoreInfo.class); } } List<String> getAvailableLayersInternal(String workspaceName, String storeName, boolean quietOnNotFound) { NamespaceInfo ns = getNamespaceInternal(workspaceName); Collection<WMSStoreInfo> stores = getStoresInternal(ns, storeName, quietOnNotFound); return stores.stream() .flatMap(store->{ WebMapServer ds; try { ds = store.getWebMapServer(null); } catch (IOException e) { throw new RestException( "Could not load wms store: " + storeName, HttpStatus.INTERNAL_SERVER_ERROR, e ); } final List<Layer> layerList = ds.getCapabilities().getLayerList(); return layerList.stream() .map(Layer::getName) .filter(Objects::nonNull) .filter(name -> !name.isEmpty()) .filter(name -> !layerConfigured(store, name)); }) .collect(Collectors.toList()); } boolean layerConfigured(final WMSStoreInfo store, final String nativeName) { final Filter filter = Predicates.and(Predicates.equal("store.name", store.getName()),Predicates.equal("nativeName", nativeName)); try(CloseableIterator<WMSLayerInfo> it = catalog.list(WMSLayerInfo.class, filter, 0, 1, null)){ return it.hasNext(); } } List<WMSLayerInfo> getConfiguredLayersInternal(String workspaceName, String storeName, boolean quietOnNotFound) { NamespaceInfo ns = getNamespaceInternal(workspaceName); Collection<WMSStoreInfo> stores = getStoresInternal(ns, storeName, quietOnNotFound); return stores.stream() .flatMap(store->catalog.getResourcesByStore(store, WMSLayerInfo.class).stream()) .collect(Collectors.toList()); } @GetMapping(value = "/{layerName}", produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_HTML_VALUE}) public RestWrapper<WMSLayerInfo> layerGet( @PathVariable String workspaceName, @PathVariable(required=false) String storeName, @PathVariable String layerName) { LOGGER.fine(()->logMessage("GET", workspaceName, storeName, layerName)); WMSLayerInfo layer = getResourceInternal(workspaceName, storeName, layerName); return wrapObject(layer, WMSLayerInfo.class); } protected NamespaceInfo getNamespaceInternal(String workspaceName) { if(Objects.isNull(workspaceName)) { throw new NullPointerException(); } else { NamespaceInfo ns = catalog.getNamespaceByPrefix(workspaceName); if(Objects.isNull(ns)) { throw new ResourceNotFoundException("Could not find workspace "+workspaceName); } else { return ns; } } } protected WMSStoreInfo getStoreInternal(NamespaceInfo ns, String storeName) { if(Objects.isNull(storeName)) { throw new NullPointerException(); } else { return catalog.getStoreByName(ns.getPrefix(), storeName, WMSStoreInfo.class); } } protected WMSLayerInfo getResourceInternal(final String workspaceName, @Nullable final String storeName, final String layerName) { final NamespaceInfo ns = getNamespaceInternal(workspaceName); final WMSLayerInfo layer; if(Objects.isNull(layerName)) { throw new NullPointerException(); } else if (Objects.isNull(storeName)) { layer = catalog.getResourceByName(ns, layerName, WMSLayerInfo.class); if(Objects.isNull(layer)) { throw new ResourceNotFoundException("No such cascaded wms: "+workspaceName+","+layerName); } else { return layer; } } else { WMSStoreInfo store = getStoreInternal(ns, storeName); layer = catalog.getResourceByStore(store, layerName, WMSLayerInfo.class); if(Objects.isNull(layer)) { throw new ResourceNotFoundException("No such cascaded wms: "+workspaceName+","+layerName); } else { return layer; } } } @PutMapping(value = "/{layerName}", consumes = { MediaType.APPLICATION_JSON_VALUE, MediaTypeExtensions.TEXT_JSON_VALUE, MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE}) public void layerPut( @RequestBody WMSLayerInfo update, @PathVariable String workspaceName, @PathVariable(required=false) String storeName, @PathVariable String layerName, @RequestParam(name = "calculate", required = false) String calculate) { LOGGER.fine(()->logMessage("PUT", workspaceName, storeName, layerName)); WMSLayerInfo original = getResourceInternal(workspaceName, storeName, layerName); calculateOptionalFields(update, original, calculate); new CatalogBuilder(catalog).updateWMSLayer(original, update); catalog.validate(original, false).throwIfInvalid(); catalog.getResourcePool().clear(original.getStore()); catalog.save(original); } @DeleteMapping(value = "/{layerName}") public void layerDelete( @PathVariable String workspaceName, @PathVariable(required=false) String storeName, @PathVariable String layerName, @RequestParam(name = "recurse", defaultValue = "false") boolean recurse) { LOGGER.fine(()->logMessage("DELETE", workspaceName, storeName, layerName)); WMSLayerInfo resource = this.getResourceInternal(workspaceName, storeName, layerName); List<LayerInfo> layers = catalog.getLayers(resource); 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( "wms layer referenced by layer(s)", HttpStatus.FORBIDDEN); } } catalog.remove( resource); } @PostMapping(consumes = { MediaType.APPLICATION_JSON_VALUE, MediaTypeExtensions.TEXT_JSON_VALUE, MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE}) public ResponseEntity<String> layerPost( @RequestBody WMSLayerInfo resource, @PathVariable String workspaceName, @PathVariable(required=false) String storeName, UriComponentsBuilder builder) throws Exception { String resourceName = handleObjectPost(resource, workspaceName, storeName); LOGGER.fine(()->logMessage("POST", workspaceName, storeName, resourceName)); UriComponents uriComponents = Objects.isNull(storeName)? builder.path("/workspaces/{workspaceName}/wmslayers/{wmslayer}") .buildAndExpand(workspaceName, resourceName): builder.path("/workspaces/{workspaceName}/wmsstores/{storeName}/wmslayers/{wmslayer}") .buildAndExpand(workspaceName, storeName, resourceName); HttpHeaders headers = new HttpHeaders(); headers.setLocation(uriComponents.toUri()); return new ResponseEntity<>(resourceName, headers, HttpStatus.CREATED); } String logMessage(final String message, final String workspaceName, @Nullable final String storeName, @Nullable final String layerName) { return message+(Objects.isNull(layerName)?"":(" WMS Layer "+layerName+" in"))+(Objects.isNull(storeName)?"":(" store "+storeName+" in"))+" in workspace "+ workspaceName; } private String handleObjectPost(WMSLayerInfo resource, String workspaceName, String storeName) throws Exception { NamespaceInfo ns = getNamespaceInternal(workspaceName); WMSStoreInfo store; if (resource.getStore() != null ) { if ( Objects.nonNull(storeName)&&!Objects.equals(storeName, resource.getStore().getName() ) ) { throw new RestException( "Expected wms store " + storeName + " but client specified " + resource.getStore().getName(), HttpStatus.FORBIDDEN ); } store=resource.getStore(); } else { store = getStoreInternal(ns, storeName); resource.setStore( store ); } //ensure workspace/namespace matches up if ( resource.getNamespace() != null ) { if ( !workspaceName.equals( resource.getNamespace().getPrefix() ) ) { throw new RestException( "Expected workspace " + workspaceName + " but client specified " + resource.getNamespace().getPrefix(), HttpStatus.FORBIDDEN ); } } else { resource.setNamespace( catalog.getNamespaceByPrefix( workspaceName ) ); } resource.setEnabled(true); NamespaceInfo foundns = resource.getNamespace(); if ( foundns != null && !foundns.getPrefix().equals( workspaceName ) ) { LOGGER.warning( "Namespace: " + ns.getPrefix() + " does not match workspace: " + workspaceName + ", overriding." ); foundns = null; } if ( foundns == null){ //infer from workspace resource.setNamespace( ns ); } // fill in missing information CatalogBuilder cb = new CatalogBuilder(catalog); cb.setStore(store); cb.initWMSLayer( resource ); resource.setEnabled(true); catalog.validate(resource, true).throwIfInvalid(); catalog.add( resource ); // create a layer for the feature type catalog.add(new CatalogBuilder(catalog).buildLayer(resource)); return resource.getName(); } // Works with the callback bellow to fix the enabled property @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return WMSLayerInfo.class.isAssignableFrom(methodParameter.getParameterType()); } @Override public void configurePersister(XStreamPersister persister, XStreamMessageConverter converter) { persister.setCallback(new XStreamPersister.Callback() { @Override protected Class<WMSLayerInfo> getObjectClass() { return WMSLayerInfo.class; } // Tries to get the object so XStream can unmarshall over top of it. // A hack to avoid overwriting the enabled property on a PUT @Override protected CatalogInfo getCatalogObject() { @SuppressWarnings("unchecked") Map<String, String> uriTemplateVars = (Map<String, String>) RequestContextHolder.getRequestAttributes().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); String workspaceName = uriTemplateVars.get("workspaceName"); String storeName = uriTemplateVars.get("storeName"); String layerName = uriTemplateVars.get("layerName"); if (workspaceName == null || storeName == null || layerName == null) { return null; } WMSStoreInfo store = catalog.getStoreByName(workspaceName, storeName, WMSStoreInfo.class); if (store == null) { return null; } return catalog.getResourceByStore(store, layerName, WMSLayerInfo.class); } @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 WMSStoreInfo) { WMSStoreInfo store = (WMSStoreInfo) obj; converter.encodeLink("/workspaces/" + converter.encode(store.getWorkspace().getName()) + "/wmsstores/" + converter.encode(store.getName()), writer); } } }); } @Override protected <T> ObjectWrapper createObjectWrapper(Class<T> clazz) { return new ObjectToMapWrapper<WMSLayerInfo>(WMSLayerInfo.class) { @SuppressWarnings({ "rawtypes", "unchecked" }) @Override protected void wrapInternal(Map properties, SimpleHash model, WMSLayerInfo object) { try { properties.put("boundingBox", object.boundingBox()); } catch (Exception e) { throw new RuntimeException(e); } } }; } }