Commit 37351f69 authored by Andrea Aime's avatar Andrea Aime
Browse files

GetStyle/PutStyle extension for WFS3, dataset level resources

parent ed5490f0
......@@ -9,4 +9,6 @@ WFS 3.0 hackaton first, and then further developed to match Draft 1 spec and con
Implementation wise:
* The module basically acts as an internal proxy around WFS 2.0, using a servlet filter to adapt protocols. The long term approach would likely be to have a new MVCDispatcher that allows usage of Spring annotations instead (TBD).
This implementation contains the following prototype WFS3 extensions:
* Tiles extension, returning MapBOX/JSON/TopoJSON tiles
* Styles extension, with the ability to get/put/delete styles (must be secured using service security)
\ No newline at end of file
......@@ -103,6 +103,18 @@
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.geoserver.extension</groupId>
<artifactId>gs-css</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.geoserver.community</groupId>
<artifactId>gs-mbstyle</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
......
/* (c) 2019 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.wfs3;
import io.swagger.v3.core.util.Yaml;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import java.util.Map;
import org.geoserver.platform.ServiceException;
public abstract class AbstractWFS3Extension implements WFS3Extension {
protected void addSchemasAndParameters(OpenAPI api, OpenAPI template) {
// and add all schemas and parameters
Components apiComponents = api.getComponents();
Components tileComponents = template.getComponents();
Map<String, Schema> tileSchemas = tileComponents.getSchemas();
apiComponents.getSchemas().putAll(tileSchemas);
Map<String, Parameter> tileParameters = tileComponents.getParameters();
apiComponents.getParameters().putAll(tileParameters);
}
/**
* Reads the template to customize (each time, as the object tree is not thread safe nor
* cloneable not serializable)
*/
protected OpenAPI readTemplate(String source) {
try {
return Yaml.mapper().readValue(source, OpenAPI.class);
} catch (Exception e) {
throw new ServiceException(e);
}
}
}
......@@ -4,67 +4,86 @@
*/
package org.geoserver.wfs3;
import static org.geoserver.ows.URLMangler.URLType.SERVICE;
import static org.geoserver.wfs3.response.ConformanceDocument.CORE;
import static org.geoserver.wfs3.response.ConformanceDocument.GEOJSON;
import static org.geoserver.wfs3.response.ConformanceDocument.GMLSF0;
import static org.geoserver.wfs3.response.ConformanceDocument.OAS30;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.HttpStatus.NO_CONTENT;
import io.swagger.v3.oas.models.OpenAPI;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.namespace.QName;
import org.apache.commons.io.IOUtils;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.NamespaceInfo;
import org.geoserver.catalog.SLDHandler;
import org.geoserver.catalog.StyleHandler;
import org.geoserver.catalog.StyleInfo;
import org.geoserver.catalog.Styles;
import org.geoserver.catalog.WorkspaceInfo;
import org.geoserver.config.GeoServer;
import org.geoserver.config.GeoServerDataDirectory;
import org.geoserver.ows.HttpErrorCodeException;
import org.geoserver.ows.LocalWorkspace;
import org.geoserver.ows.Response;
import org.geoserver.ows.util.ResponseUtils;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.ServiceException;
import org.geoserver.wfs.StoredQueryProvider;
import org.geoserver.wfs.WFSGetFeatureOutputFormat;
import org.geoserver.wfs.WFSInfo;
import org.geoserver.wfs.WebFeatureService20;
import org.geoserver.wfs.request.FeatureCollectionResponse;
import org.geoserver.wfs.request.GetFeatureRequest;
import org.geoserver.wfs3.response.CollectionDocument;
import org.geoserver.wfs3.response.CollectionsDocument;
import org.geoserver.wfs3.response.ConformanceDocument;
import org.geoserver.wfs3.response.LandingPageDocument;
import org.geoserver.wfs3.response.Link;
import org.geoserver.wfs3.response.StyleDocument;
import org.geoserver.wfs3.response.StylesDocument;
import org.geoserver.wfs3.response.TilingSchemeDescriptionDocument;
import org.geoserver.wfs3.response.TilingSchemesDocument;
import org.geotools.util.logging.Logging;
import org.geotools.styling.Style;
import org.geotools.styling.StyledLayerDescriptor;
import org.geowebcache.config.DefaultGridsets;
import org.opengis.filter.FilterFactory2;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
/** WFS 3.0 implementation */
public class DefaultWebFeatureService30 implements WebFeatureService30, ApplicationContextAware {
private static final Logger LOGGER = Logging.getLogger(DefaultWebFeatureService30.class);
private final GeoServerDataDirectory dataDirectory;
private FilterFactory2 filterFactory;
private final GeoServer geoServer;
private WebFeatureService20 wfs20;
private final DefaultGridsets gridSets;
private List<WFS3Extension> extensions;
public DefaultWebFeatureService30(GeoServer geoServer, WebFeatureService20 wfs20) {
this(geoServer, wfs20, new DefaultGridsets(true, true));
}
public DefaultWebFeatureService30(
GeoServer geoServer, WebFeatureService20 wfs20, DefaultGridsets gridSets) {
public DefaultWebFeatureService30(GeoServer geoServer, DefaultGridsets gridSets) {
this.geoServer = geoServer;
this.wfs20 = wfs20;
this.gridSets = gridSets;
this.dataDirectory = new GeoServerDataDirectory(geoServer.getCatalog().getResourceLoader());
}
public FilterFactory2 getFilterFactory() {
......@@ -185,7 +204,7 @@ public class DefaultWebFeatureService30 implements WebFeatureService30, Applicat
@Override
public TilingSchemesDocument tilingSchemes(TilingSchemesRequest request) {
return new TilingSchemesDocument(geoServer, gridSets);
return new TilingSchemesDocument(gridSets);
}
@Override
......@@ -202,10 +221,225 @@ public class DefaultWebFeatureService30 implements WebFeatureService30, Applicat
@Override
public TilingSchemeDescriptionDocument describeTilingScheme(
TilingSchemeDescriptionRequest request) {
return new TilingSchemeDescriptionDocument(geoServer, request.getGridSet());
return new TilingSchemeDescriptionDocument(request.getGridSet());
}
public void setApplicationContext(ApplicationContext context) throws BeansException {
extensions = GeoServerExtensions.extensions(WFS3Extension.class, context);
}
@Override
public void postStyles(HttpServletRequest request, HttpServletResponse response)
throws IOException {
final String mimeType = request.getContentType();
final StyleHandler handler = Styles.handler(mimeType);
if (handler == null) {
throw new HttpErrorCodeException(
HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(),
"Cannot handle a style of type " + mimeType);
}
final Catalog catalog = getCatalog();
final String styleBody = IOUtils.toString(request.getReader());
final StyledLayerDescriptor sld =
handler.parse(
styleBody,
handler.versionForMimeType(mimeType),
dataDirectory.getResourceLocator(),
catalog.getResourcePool().getEntityResolver());
String name = getStyleName(sld);
if (name == null) {
throw new HttpErrorCodeException(
HttpStatus.BAD_REQUEST.value(), "Style does not have a name!");
}
final WorkspaceInfo wsInfo = LocalWorkspace.get();
if (catalog.getStyleByName(wsInfo, name) != null) {
throw new HttpErrorCodeException(
HttpStatus.BAD_REQUEST.value(), "Style already exists!");
}
StyleInfo sinfo = catalog.getFactory().createStyle();
sinfo.setName(name);
sinfo.setFilename(name + "." + handler.getFileExtension());
sinfo.setFormat(handler.getFormat());
sinfo.setFormatVersion(handler.versionForMimeType(mimeType));
if (wsInfo != null) {
sinfo.setWorkspace(wsInfo);
}
try {
catalog.getResourcePool()
.writeStyle(sinfo, new ByteArrayInputStream(styleBody.getBytes()));
} catch (Exception e) {
throw new HttpErrorCodeException(INTERNAL_SERVER_ERROR.value(), "Error writing style");
}
catalog.add(sinfo);
// build and return the new path
ResponseUtils.appendPath(request.getContextPath());
final String baseURL = ResponseUtils.baseURL(request);
final String url =
ResponseUtils.buildURL(
// URLType.SERVICE is important here, otherwise no ws localization
baseURL, "wfs3/styles/" + name, null, SERVICE);
response.setStatus(HttpStatus.CREATED.value());
response.addHeader(HttpHeaders.LOCATION, url);
}
public String getStyleName(StyledLayerDescriptor sld) {
String name = sld.getName();
if (name == null) {
Style style = Styles.style(sld);
name = style.getName();
}
return name;
}
@Override
public StylesDocument getStyles(GetStylesRequest request) throws IOException {
List<StyleDocument> styles = new ArrayList<>();
// return only styles that are not associated to a layer, those will show up
// in the layer association instead
final Set<StyleInfo> blacklist = getLayerAssociatedStyles();
addBuiltInStyles(blacklist);
for (StyleInfo style : getCatalog().getStyles()) {
if (blacklist.contains(style)) {
continue;
}
StyleDocument sd = StyleDocument.build(style);
String styleFormat = style.getFormat();
if (styleFormat == null) {
styleFormat = "SLD";
}
// canonicalize
styleFormat = Styles.handler(styleFormat).mimeType(null);
// add link for native format
sd.addLink(buildLink(request, sd, styleFormat));
if (!SLDHandler.MIMETYPE_10.equalsIgnoreCase(styleFormat)) {
// add link for SLD 1.0 translation
sd.addLink(buildLink(request, sd, SLDHandler.MIMETYPE_10));
}
styles.add(sd);
}
return new StylesDocument(styles);
}
public void addBuiltInStyles(Set<StyleInfo> blacklist) {
accumulateStyle(blacklist, getCatalog().getStyleByName(StyleInfo.DEFAULT_POINT));
accumulateStyle(blacklist, getCatalog().getStyleByName(StyleInfo.DEFAULT_LINE));
accumulateStyle(blacklist, getCatalog().getStyleByName(StyleInfo.DEFAULT_POLYGON));
accumulateStyle(blacklist, getCatalog().getStyleByName(StyleInfo.DEFAULT_GENERIC));
accumulateStyle(blacklist, getCatalog().getStyleByName(StyleInfo.DEFAULT_RASTER));
}
public Link buildLink(GetStylesRequest request, StyleDocument sd, String styleFormat) {
String href =
ResponseUtils.buildURL(
request.getBaseUrl(),
"wfs3/styles/" + sd.getId(),
Collections.singletonMap("f", styleFormat),
SERVICE);
return new Link(href, "style", styleFormat, null);
}
/**
* Returns a list of styles that are not associated with any layer
*
* @return
*/
private Set<StyleInfo> getLayerAssociatedStyles() {
Set<StyleInfo> result = new HashSet<>();
for (LayerInfo layer : getCatalog().getLayers()) {
accumulateStyle(result, layer.getDefaultStyle());
if (layer.getStyles() != null) {
for (StyleInfo style : layer.getStyles()) {
accumulateStyle(result, style);
}
}
}
return result;
}
private void accumulateStyle(Set<StyleInfo> result, StyleInfo style) {
if (style != null) {
result.add(style);
}
}
@Override
public StyleInfo getStyle(GetStyleRequest request) throws IOException {
final StyleInfo style = getCatalog().getStyleByName(request.getStyleId());
if (style == null) {
throw new HttpErrorCodeException(
NOT_FOUND.value(), "Style " + request.getStyleId() + " could not be found");
}
return style;
}
@Override
public void putStyle(
HttpServletRequest request, HttpServletResponse response, PutStyleRequest putStyle)
throws IOException {
final String mimeType = request.getContentType();
final StyleHandler handler = Styles.handler(mimeType);
if (handler == null) {
throw new HttpErrorCodeException(
HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(),
"Cannot handle a style of type " + mimeType);
}
final Catalog catalog = getCatalog();
final String styleBody = IOUtils.toString(request.getReader());
final WorkspaceInfo wsInfo = LocalWorkspace.get();
String name = putStyle.getStyleId();
StyleInfo sinfo = catalog.getStyleByName(wsInfo, name);
boolean newStyle = sinfo == null;
if (newStyle) {
sinfo = catalog.getFactory().createStyle();
sinfo.setName(name);
sinfo.setFilename(name + "." + handler.getFileExtension());
}
sinfo.setFormat(handler.getFormat());
sinfo.setFormatVersion(handler.versionForMimeType(mimeType));
if (wsInfo != null) {
sinfo.setWorkspace(wsInfo);
}
try {
catalog.getResourcePool()
.writeStyle(sinfo, new ByteArrayInputStream(styleBody.getBytes()));
} catch (Exception e) {
throw new HttpErrorCodeException(INTERNAL_SERVER_ERROR.value(), "Error writing style");
}
if (newStyle) {
catalog.add(sinfo);
} else {
catalog.save(sinfo);
}
response.setStatus(NO_CONTENT.value());
}
@Override
public void deleteStyle(DeleteStyleRequest request, HttpServletResponse response)
throws IOException {
String name = request.getStyleId();
WorkspaceInfo ws = LocalWorkspace.get();
StyleInfo sinfo = getCatalog().getStyleByName(ws, name);
if (sinfo == null) {
throw new HttpErrorCodeException(
NOT_FOUND.value(), "Could not find style with id: " + name);
}
getCatalog().remove(sinfo);
response.setStatus(NO_CONTENT.value());
}
}
/* (c) 2018 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.wfs3;
/** Request to remove a specific style */
public class DeleteStyleRequest extends BaseRequest {
String styleId;
String collectionId;
public String getStyleId() {
return styleId;
}
public void setStyleId(String styleId) {
this.styleId = styleId;
}
public String getCollectionId() {
return collectionId;
}
public void setCollectionId(String collectionId) {
this.collectionId = collectionId;
}
}
/* (c) 2018 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.wfs3;
/** Request for a specific style */
public class GetStyleRequest extends BaseRequest {
String styleId;
String collectionId;
public String getStyleId() {
return styleId;
}
public void setStyleId(String styleId) {
this.styleId = styleId;
}
public String getCollectionId() {
return collectionId;
}
public void setCollectionId(String collectionId) {
this.collectionId = collectionId;
}
}
/* (c) 2018 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.wfs3;
/** Request for the server contents */
public class GetStylesRequest extends BaseRequest {}
/* (c) 2018 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.wfs3;
public class PutStyleRequest extends BaseRequest {
String styleId;
String collectionId;
public String getStyleId() {
return styleId;
}
public void setStyleId(String styleId) {
this.styleId = styleId;
}
public String getCollectionId() {
return collectionId;
}
public void setCollectionId(String collectionId) {
this.collectionId = collectionId;
}
}
/* (c) 2018 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.wfs3;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.geoserver.catalog.SLDPackageHandler;
import org.geoserver.catalog.StyleHandler;
import org.geoserver.ows.URLMangler;
import org.geoserver.ows.util.ResponseUtils;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.util.IOUtils;
import org.geoserver.wfs3.response.CollectionDocument;
import org.geoserver.wfs3.response.Link;
import org.geotools.util.Version;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
/** WFS3 extension adding support for get/put styles (as per OGC VTP pilot) */
public class StylesExtension extends AbstractWFS3Extension implements ApplicationContextAware {
private static final String STYLES_SPECIFICATION;
static final String STYLES_PATH = "/styles";
static final String STYLE_PATH = "/styles/{styleId}";
static final String COLLECTION_STYLES_PATH = "/collections/{collectionId}/styles";
static final String COLLECTION_STYLE_PATH = "/collections/{collectionId}/styles/{styleId}";
static {
try (InputStream is = StylesExtension.class.getResourceAsStream("styles.yml")) {
STYLES_SPECIFICATION = IOUtils.toString(is);
} catch (Exception e) {
throw new RuntimeException("Failed to read the styles.yaml template", e);
}
}
private ArrayList<String> formats;
@Override
public void extendAPI(OpenAPI api) {
// load the pre-cooked building blocks
OpenAPI template = readTemplate(StylesExtension.STYLES_SPECIFICATION);
copyAndCustomize(template, api, STYLES_PATH);
copyAndCustomize(template, api, STYLE_PATH);
copyAndCustomize(template, api, COLLECTION_STYLES_PATH);
copyAndCustomize(template, api, COLLECTION_STYLE_PATH);
addSchemasAndParameters(api, template);
}
public void copyAndCustomize(OpenAPI template, OpenAPI target, String stylesPath) {
PathItem pathItem = template.getPaths().get(stylesPath);
customizeContentTypes(pathItem.getPost());
customizeContentTypes