Commit 4bedc4c8 authored by Andrea Aime's avatar Andrea Aime
Browse files

Style operations on single collection too

parent 9a8989a2
......@@ -10,5 +10,5 @@ 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
* Tiles extension, returning MapBOX/JSON/TopoJSON tiles. It does not cache tiles and will likely be removed when WFS3 is pushed to supported land, but served as a base to study a possible WMTS 2.0 API and WFS 3 tile extension in Testbed 14. Some bits can likely be re-used once a final version comes out.
* Styles extension, with the ability to get/put/delete styles (must be secured using service security). The API is not really compatible with GeoServer style management and should also be removed, but was used to provide feedback to OGC in the vector tiles pilot, which will likely be used for WMTS 2.0 (and some bits can likely be re-used once a final version comes out).
\ No newline at end of file
......@@ -25,12 +25,15 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
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.CatalogBuilder;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.NamespaceInfo;
......@@ -229,7 +232,8 @@ public class DefaultWebFeatureService30 implements WebFeatureService30, Applicat
}
@Override
public void postStyles(HttpServletRequest request, HttpServletResponse response)
public void postStyles(
HttpServletRequest request, HttpServletResponse response, PostStyleRequest post)
throws IOException {
final String mimeType = request.getContentType();
final StyleHandler handler = Styles.handler(mimeType);
......@@ -276,13 +280,28 @@ public class DefaultWebFeatureService30 implements WebFeatureService30, Applicat
catalog.add(sinfo);
// do we need to associate with a layer?
LayerInfo layer = null;
if (post.getLayerName() != null) {
// layer existence already checked in WFS3Filter
layer = getCatalog().getLayerByName(post.getLayerName());
layer.getStyles().add(sinfo);
getCatalog().save(layer);
}
// 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);
final String path =
layer == null
? "wfs3/styles/" + name
: "wfs3/collections/"
+ NCNameResourceCodec.encode(layer.getResource())
+ "/styles/"
+ name;
// URLType.SERVICE is important here, otherwise no ws localization
final String url = ResponseUtils.buildURL(baseURL, path, null, SERVICE);
response.setStatus(HttpStatus.CREATED.value());
response.addHeader(HttpHeaders.LOCATION, url);
}
......@@ -300,34 +319,55 @@ public class DefaultWebFeatureService30 implements WebFeatureService30, Applicat
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;
if (request.getLayerName() == null) {
// 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 = buildStyleDocument(request, style);
styles.add(sd);
}
StyleDocument sd = StyleDocument.build(style);
String styleFormat = style.getFormat();
if (styleFormat == null) {
styleFormat = "SLD";
} else {
final LayerInfo layer = getCatalog().getLayerByName(request.getLayerName());
if (layer.getDefaultStyle() != null) {
StyleDocument sd = buildStyleDocument(request, layer.getDefaultStyle());
styles.add(sd);
}
// 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));
if (layer.getStyles() != null) {
for (StyleInfo style : layer.getStyles()) {
if (style != null) {
StyleDocument sd = buildStyleDocument(request, style);
styles.add(sd);
}
}
}
styles.add(sd);
}
return new StylesDocument(styles);
}
public StyleDocument buildStyleDocument(GetStylesRequest request, StyleInfo style) {
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));
}
return sd;
}
public void addBuiltInStyles(Set<StyleInfo> blacklist) {
accumulateStyle(blacklist, getCatalog().getStyleByName(StyleInfo.DEFAULT_POINT));
accumulateStyle(blacklist, getCatalog().getStyleByName(StyleInfo.DEFAULT_LINE));
......@@ -337,10 +377,18 @@ public class DefaultWebFeatureService30 implements WebFeatureService30, Applicat
}
public Link buildLink(GetStylesRequest request, StyleDocument sd, String styleFormat) {
String path;
if (request.getLayerName() != null) {
FeatureTypeInfo featureType = getCatalog().getFeatureTypeByName(request.getLayerName());
String collectionId = NCNameResourceCodec.encode(featureType);
path = "wfs3/collections/" + collectionId + "/styles/" + sd.getId();
} else {
path = "wfs3/styles/" + sd.getId();
}
String href =
ResponseUtils.buildURL(
request.getBaseUrl(),
"wfs3/styles/" + sd.getId(),
path,
Collections.singletonMap("f", styleFormat),
SERVICE);
return new Link(href, "style", styleFormat, null);
......@@ -373,10 +421,32 @@ public class DefaultWebFeatureService30 implements WebFeatureService30, Applicat
@Override
public StyleInfo getStyle(GetStyleRequest request) throws IOException {
final StyleInfo style = getCatalog().getStyleByName(request.getStyleId());
StyleInfo style = null;
LayerInfo layer = null;
if (request.getLayerName() != null) {
// layer existence already checked in WFSFilter
layer = getCatalog().getLayerByName(request.getLayerName());
if (layer.getDefaultStyle() != null
&& layer.getDefaultStyle().getName().equals(request.getStyleId())) {
style = layer.getDefaultStyle();
} else {
Predicate<StyleInfo> styleFilter =
s -> s != null && s.getName().equalsIgnoreCase(request.getStyleId());
Optional<StyleInfo> first =
layer.getStyles().stream().filter(styleFilter).findFirst();
if (first.isPresent()) {
style = first.get();
}
}
} else {
style = getCatalog().getStyleByName(request.getStyleId());
}
if (style == null) {
throw new HttpErrorCodeException(
NOT_FOUND.value(), "Style " + request.getStyleId() + " could not be found");
String message = "Style " + request.getStyleId() + " could not be found";
if (layer != null) {
message += " in collection " + NCNameResourceCodec.encode(layer.getResource());
}
throw new HttpErrorCodeException(NOT_FOUND.value(), message);
}
return style;
......@@ -397,13 +467,13 @@ public class DefaultWebFeatureService30 implements WebFeatureService30, Applicat
final String styleBody = IOUtils.toString(request.getReader());
final WorkspaceInfo wsInfo = LocalWorkspace.get();
String name = putStyle.getStyleId();
StyleInfo sinfo = catalog.getStyleByName(wsInfo, name);
String styleName = putStyle.getStyleId();
StyleInfo sinfo = catalog.getStyleByName(wsInfo, styleName);
boolean newStyle = sinfo == null;
if (newStyle) {
sinfo = catalog.getFactory().createStyle();
sinfo.setName(name);
sinfo.setFilename(name + "." + handler.getFileExtension());
sinfo.setName(styleName);
sinfo.setFilename(styleName + "." + handler.getFileExtension());
}
sinfo.setFormat(handler.getFormat());
......@@ -425,6 +495,17 @@ public class DefaultWebFeatureService30 implements WebFeatureService30, Applicat
catalog.save(sinfo);
}
final String layerName = putStyle.getLayerName();
if (layerName != null) {
final LayerInfo layer = catalog.getLayerByName(layerName);
if (!layer.getStyles()
.stream()
.anyMatch(s -> s != null && s.getName().equalsIgnoreCase(styleName))) {
layer.getStyles().add(sinfo);
catalog.save(layer);
}
}
response.setStatus(NO_CONTENT.value());
}
......@@ -433,12 +514,33 @@ public class DefaultWebFeatureService30 implements WebFeatureService30, Applicat
throws IOException {
String name = request.getStyleId();
WorkspaceInfo ws = LocalWorkspace.get();
StyleInfo sinfo = getCatalog().getStyleByName(ws, name);
final Catalog catalog = getCatalog();
StyleInfo sinfo = catalog.getStyleByName(ws, name);
if (sinfo == null) {
throw new HttpErrorCodeException(
NOT_FOUND.value(), "Could not find style with id: " + name);
}
getCatalog().remove(sinfo);
if (request.getLayerName() != null) {
LayerInfo layer = catalog.getLayerByName(request.getLayerName());
if (sinfo.equals(layer.getDefaultStyle())) {
StyleInfo newDefault =
new CatalogBuilder(catalog).getDefaultStyle(layer.getResource());
layer.setDefaultStyle(newDefault);
} else {
if (!(layer.getStyles().remove(sinfo))) {
throw new HttpErrorCodeException(
NOT_FOUND.value(),
"Style with id: "
+ name
+ " is not associated to collection "
+ NCNameResourceCodec.encode(layer.getResource()));
}
}
catalog.save(layer);
}
catalog.remove(sinfo);
response.setStatus(NO_CONTENT.value());
}
......
......@@ -9,7 +9,7 @@ public class DeleteStyleRequest extends BaseRequest {
String styleId;
String collectionId;
String layerName;
public String getStyleId() {
return styleId;
......@@ -19,11 +19,11 @@ public class DeleteStyleRequest extends BaseRequest {
this.styleId = styleId;
}
public String getCollectionId() {
return collectionId;
public String getLayerName() {
return layerName;
}
public void setCollectionId(String collectionId) {
this.collectionId = collectionId;
public void setLayerName(String layerName) {
this.layerName = layerName;
}
}
......@@ -9,7 +9,7 @@ public class GetStyleRequest extends BaseRequest {
String styleId;
String collectionId;
String layerName;
public String getStyleId() {
return styleId;
......@@ -19,11 +19,11 @@ public class GetStyleRequest extends BaseRequest {
this.styleId = styleId;
}
public String getCollectionId() {
return collectionId;
public String getLayerName() {
return layerName;
}
public void setCollectionId(String collectionId) {
this.collectionId = collectionId;
public void setLayerName(String layerName) {
this.layerName = layerName;
}
}
......@@ -5,4 +5,15 @@
package org.geoserver.wfs3;
/** Request for the server contents */
public class GetStylesRequest extends BaseRequest {}
public class GetStylesRequest extends BaseRequest {
String layerName;
public String getLayerName() {
return layerName;
}
public void setLayerName(String layerName) {
this.layerName = layerName;
}
}
/* (c) 201 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 PostStyleRequest extends BaseRequest {
String layerName;
public String getLayerName() {
return layerName;
}
public void setLayerName(String layerName) {
this.layerName = layerName;
}
}
......@@ -6,7 +6,7 @@ package org.geoserver.wfs3;
public class PutStyleRequest extends BaseRequest {
String styleId;
String collectionId;
String layerName;
public String getStyleId() {
return styleId;
......@@ -16,11 +16,11 @@ public class PutStyleRequest extends BaseRequest {
this.styleId = styleId;
}
public String getCollectionId() {
return collectionId;
public String getLayerName() {
return layerName;
}
public void setCollectionId(String collectionId) {
this.collectionId = collectionId;
public void setLayerName(String layerName) {
this.layerName = layerName;
}
}
......@@ -76,7 +76,10 @@ public class WFS3DispatcherCallback extends AbstractDispatcherCallback {
} else if ("api".equals(request.getRequest())) {
defaultType = OpenAPIResponse.OPEN_API_MIME;
}
setOutputFormat(request, parsedRequest, formatSetter, defaultType);
// for getStyle we're going to use the "native" format if possible
if (!"getStyle".equals(request.getRequest())) {
setOutputFormat(request, parsedRequest, formatSetter, defaultType);
}
}
}
} catch (Exception e) {
......
......@@ -124,13 +124,19 @@ public class WFS3Filter implements GeoServerFilter {
Pattern.compile("/collections/([^/]+)/tiles/([^/]+)/?").matcher(pathInfo);
matcher.matches();
this.tilingScheme = matcher.group(2);
} else if (pathInfo.matches("/collections/([^/]+)/styles/?")) {
request = wrapped.getMethod().toLowerCase() + "Styles";
Matcher matcher =
Pattern.compile("/collections/([^/]+)/styles/?").matcher(pathInfo);
matcher.matches();
setLayerName(matcher.group(1));
} else if (pathInfo.matches("/collections/([^/]+)/styles/([^/]+)/?")) {
request = wrapped.getMethod().toLowerCase() + "Style";
Matcher matcher =
Pattern.compile("/collections/([^/]+)/tiles/([^/]+)/?").matcher(pathInfo);
Pattern.compile("/collections/([^/]+)/styles/([^/]+)/?").matcher(pathInfo);
matcher.matches();
this.typeName = matcher.group(1);
this.tilingScheme = matcher.group(2);
setLayerName(matcher.group(1));
this.styleId = matcher.group(2);
} else if (pathInfo.startsWith("/collections")) {
List<Function<String, Boolean>> matchers = new ArrayList<>();
matchers.add(
......@@ -345,8 +351,12 @@ public class WFS3Filter implements GeoServerFilter {
}
}
if (typeName != null) {
filtered.put("typeName", new String[] {typeName});
filtered.put("typeNames", new String[] {typeName});
if (request.toLowerCase().contains("style")) {
filtered.put("layerName", new String[] {typeName});
} else {
filtered.put("typeName", new String[] {typeName});
filtered.put("typeNames", new String[] {typeName});
}
}
if (outputFormat != null) {
filtered.put("outputFormat", new String[] {outputFormat});
......
......@@ -84,7 +84,8 @@ public interface WebFeatureService30 {
StyleInfo getStyle(GetStyleRequest request) throws IOException;
void postStyles(HttpServletRequest request, HttpServletResponse response) throws IOException;
void postStyles(HttpServletRequest request, HttpServletResponse response, PostStyleRequest post)
throws IOException;
void putStyle(
HttpServletRequest request, HttpServletResponse response, PutStyleRequest putStyle)
......
/*
* (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.kvp;
import org.geoserver.wfs3.PostStyleRequest;
/** Parses a "PostStyle" request */
public class PostStyleRequestKVPReader extends BaseKvpRequestReader {
public PostStyleRequestKVPReader() {
super(PostStyleRequest.class);
}
}
......@@ -103,6 +103,7 @@
<bean id="getStylesKvpRequestReader" class="org.geoserver.wfs3.kvp.GetStylesRequestKVPReader"/>
<bean id="getStyleKvpRequestReader" class="org.geoserver.wfs3.kvp.GetStyleRequestKVPReader"/>
<bean id="putStyleKvpRequestReader" class="org.geoserver.wfs3.kvp.PutStyleRequestKVPReader"/>
<bean id="postStyleKvpRequestReader" class="org.geoserver.wfs3.kvp.PostStyleRequestKVPReader"/>
<bean id="deleteStyleKvpRequestReader" class="org.geoserver.wfs3.kvp.DeleteStyleRequestKVPReader"/>
<!-- response generation -->
......
......@@ -5,7 +5,11 @@
package org.geoserver.wfs3;
import static org.custommonkey.xmlunit.XMLAssert.assertXpathEvaluatesTo;
import static org.geoserver.data.test.MockData.ROAD_SEGMENTS;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasProperty;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
......@@ -30,6 +34,7 @@ import java.util.Optional;
import java.util.logging.Level;
import org.apache.commons.io.IOUtils;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.PropertyStyleHandler;
import org.geoserver.catalog.SLDHandler;
import org.geoserver.catalog.StyleInfo;
......@@ -52,12 +57,21 @@ public class StyleTest extends WFS3TestSupport {
@Override
protected void onSetUp(SystemTestData testData) throws Exception {
super.onSetUp(testData);
testData.addStyle("dashed", "dashedline.sld", StyleTest.class, getCatalog());
final Catalog catalog = getCatalog();
testData.addStyle("dashed", "dashedline.sld", StyleTest.class, catalog);
final LayerInfo roadSegments = catalog.getLayerByName(getLayerId(ROAD_SEGMENTS));
catalog.save(roadSegments);
}
@Before
public void cleanNewStyles() {
public void cleanup() {
final Catalog catalog = getCatalog();
final LayerInfo roadSegments = catalog.getLayerByName(getLayerId(ROAD_SEGMENTS));
roadSegments.getStyles().clear();
StyleInfo line = catalog.getStyleByName("line");
roadSegments.getStyles().add(line);
catalog.save(roadSegments);
Optional.ofNullable(catalog.getStyleByName("simplePoint"))
.ifPresent(s -> catalog.remove(s));
Optional.ofNullable(catalog.getStyleByName("testPoint")).ifPresent(s -> catalog.remove(s));
......@@ -79,6 +93,29 @@ public class StyleTest extends WFS3TestSupport {
.get(0));
}
@Test
public void testGetCollectionStyles() throws Exception {
String roadSegments = getEncodedName(ROAD_SEGMENTS);
final DocumentContext doc =
getAsJSONPath("wfs3/collections/" + roadSegments + "/styles", 200);
// two stiles, the native one, and the line associated one
assertEquals(Integer.valueOf(2), doc.read("styles.length()", Integer.class));
assertEquals("RoadSegments", doc.read("styles..id", List.class).get(0));
assertEquals(
"http://localhost:8080/geoserver/wfs3/collections/cite__RoadSegments/styles/RoadSegments?f=application%2Fvnd.ogc.sld%2Bxml",
doc.read(
"styles..links[?(@.rel=='style' && @.type=='application/vnd.ogc.sld+xml')].href",
List.class)
.get(0));
assertEquals("line", doc.read("styles..id", List.class).get(1));
assertEquals(
"http://localhost:8080/geoserver/wfs3/collections/cite__RoadSegments/styles/line?f=application%2Fvnd.ogc.sld%2Bxml",
doc.read(
"styles..links[?(@.rel=='style' && @.type=='application/vnd.ogc.sld+xml')].href",
List.class)
.get(1));
}
@Test
public void testGetStyle() throws Exception {
final MockHttpServletResponse response = getAsServletResponse("wfs3/styles/dashed?f=sld");
......@@ -94,6 +131,28 @@ public class StyleTest extends WFS3TestSupport {
dom);
}
@Test
public void testGetCollectionStyle() throws Exception {
final MockHttpServletResponse response =
getAsServletResponse("wfs3/collections/cite__RoadSegments/styles/line");
assertEquals(OK.value(), response.getStatus());
assertEquals(SLDHandler.MIMETYPE_10, response.getContentType());
final Document dom = dom(response, true);
// print(dom);
assertXpathEvaluatesTo("A boring default style", "//sld:UserStyle/sld:Title", dom);
assertXpathEvaluatesTo("1", "count(//sld:Rule)", dom);
assertXpathEvaluatesTo("1", "count(//sld:LineSymbolizer)", dom);
assertXpathEvaluatesTo(
"#0000FF", "//sld:LineSymbolizer/sld:Stroke/sld:CssParameter[@name='stroke']", dom);
}
@Test