Skip to content

Commit

Permalink
[GEOS-8382] Add isolated workspaces concept to GeoServer
Browse files Browse the repository at this point in the history
  • Loading branch information
Nuno Oliveira committed Feb 16, 2018
1 parent f977e78 commit aedef5e
Show file tree
Hide file tree
Showing 65 changed files with 3,450 additions and 836 deletions.
Binary file modified doc/en/user/source/data/webadmin/img/data_workspaces.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified doc/en/user/source/data/webadmin/img/data_workspaces_URI.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
58 changes: 57 additions & 1 deletion doc/en/user/source/data/webadmin/workspaces.rst
Expand Up @@ -56,4 +56,60 @@ To remove a workspace, select it by clicking the checkbox next to the workspace.
.. figure:: img/data_workspaces_rename_confirm.png .. figure:: img/data_workspaces_rename_confirm.png


Workspace removal confirmation Workspace removal confirmation


Isolated Workspaces
-------------------

Isolated workspaces content is only visible and queryable in the context of a virtual service bound to the isolated workspace. This means that isolated workspaces content will not show up in global capabilities documents and global services cannot query isolated workspaces contents. Is worth mentioning that those restrictions don't apply to the REST API.

A workspace can be made isolated by checking the :guilabel:`Isolated Workspace` checkbox when creating or editing a workspace.

.. figure:: img/isolated_workspace.png

Making a workspace isolated

An isolated workspace will be able to reuse a namespace already used by another workspace, but its resources (layers, styles, etc ...) can only be retrieved when using that workspace virtual services and will only show up in those virtual services capabilities documents.

It is only possible to create two or more workspaces with the same namespace in GeoServer if only one of them is non isolated, i.e. isolated workspaces have no restrictions in namespaces usage but two non isolated workspaces can't use the same namespace.

The following situation will be valid:

- Prefix: st1 Namespace: http://www.stations.org/1.0 Isolated: false

- Prefix: st2 Namespace: http://www.stations.org/1.0 Isolated: true

- Prefix: st3 Namespace: http://www.stations.org/1.0 Isolated: true

But not the following one:

- Prefix: st1 Namespace: http://www.stations.org/1.0 Isolated: false

- **Prefix: st2 Namespace: http://www.stations.org/1.0 Isolated: false**

- Prefix: st3 Namespace: http://www.stations.org/1.0 Isolated: true

At most only one non isolated workspace can use a certain namespace.

Consider the following image which shows to workspaces (st1 and st2) that use the same namespace (http://www.stations.org/1.0) and several layers contained by them:

.. figure:: img/workspaces_example.png

Two workspaces using the same namespace, one of them is isolated.

In the example above st2 is the isolated workspace. Consider the following WFS GetFeature requests:

1. http://localhost:8080/geoserver/ows?service=WFS&version=2.0.0&request=DescribeFeatureType&typeName=layer2

2. http://localhost:8080/geoserver/st2/ows?service=WFS&version=2.0.0&request=DescribeFeatureType&typeName=layer2

3. http://localhost:8080/geoserver/ows?service=WFS&version=2.0.0&request=DescribeFeatureType&typeName=st1:layer2

4. http://localhost:8080/geoserver/st2/ows?service=WFS&version=2.0.0&request=DescribeFeatureType&typeName=st2:layer2

5. http://localhost:8080/geoserver/ows?service=WFS&version=2.0.0&request=DescribeFeatureType&typeName=st2:layer2

6. http://localhost:8080/geoserver/ows?service=WFS&version=2.0.0&request=DescribeFeatureType&typeName=layer5

The first request is targeting WFS global service and requesting layer2, this request will use layer2 contained by workspace st1. The second request is targeting st2 workspace WFS virtual service, layer2 belonging to workspace st2 will be used. Request three and four will use layer2 belonging to workspace, respectively, st1 and st2. The last two requests will fail saying that the feature type was not found, isolated workspaces content is not visible globally.

**The rule of thumb is that resources (layers, styles, etc ...) belonging to an isolated workspace can only be retrieved when using that workspaces virtual services and will only show up in those virtual services capabilities documents.**
Expand Up @@ -17,9 +17,11 @@
import java.io.Serializable; import java.io.Serializable;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;


Expand Down Expand Up @@ -149,6 +151,8 @@ public abstract class AbstractAppSchemaMockData extends SystemTestData
private final Map<String, String> datastoreNamespacePrefixes = new LinkedHashMap<String, String>(); private final Map<String, String> datastoreNamespacePrefixes = new LinkedHashMap<String, String>();


private final Map<String, String> namespaces; private final Map<String, String> namespaces;

private final List<String> isolatedNamespaces = new ArrayList<>();


private final Map<String, String> layerStyles = new LinkedHashMap<String,String>(); private final Map<String, String> layerStyles = new LinkedHashMap<String,String>();


Expand Down Expand Up @@ -359,7 +363,7 @@ private void setUpCatalog() {
.<String> emptySet()); .<String> emptySet());
writer.coverageStores(new HashMap<String, Map<String, String>>(), writer.coverageStores(new HashMap<String, Map<String, String>>(),
new HashMap<String, String>(), Collections.<String> emptySet()); new HashMap<String, String>(), Collections.<String> emptySet());
writer.namespaces(namespaces); writer.namespaces(namespaces, isolatedNamespaces);
writer.styles(layerStyles); writer.styles(layerStyles);
try { try {
writer.write(new File(data, "catalog.xml")); writer.write(new File(data, "catalog.xml"));
Expand Down Expand Up @@ -625,14 +629,26 @@ protected void putNamespace(String namspacePrefix, String namespaceUri) {
namespaces.put(namspacePrefix, namespaceUri); namespaces.put(namspacePrefix, namespaceUri);
} }


/**
* Put an isolated namespace into the namespaces map.
*
* @param namespacePrefix namespace prefix
* @param namespaceUri namespace URI
*/
protected void putIsolatedNamespace(String namespacePrefix, String namespaceUri) {
namespaces.put(namespacePrefix, namespaceUri);
isolatedNamespaces.add(namespacePrefix);
}

/** /**
* Remove a namespace in a map. * Remove a namespace in a map.
* *
* @param namspacePrefix * @param namespacePrefix
* namespace prefix * namespace prefix
*/ */
protected void removeNamespace(String namspacePrefix) { protected void removeNamespace(String namespacePrefix) {
namespaces.remove(namspacePrefix); namespaces.remove(namespacePrefix);
isolatedNamespaces.remove(namespacePrefix);
} }


/** /**
Expand Down
@@ -0,0 +1,209 @@

/* (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.test;

import org.custommonkey.xmlunit.XpathEngine;
import org.junit.Before;
import org.junit.Test;
import org.w3c.dom.Document;

import java.util.HashMap;
import java.util.Map;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

/**
* Tests that isolated workspaces \ namespaces allow the publishing of the same complex
* feature type multiple times.
*
* The tests use three three different types of mappings which allow us to test three
* particular situations, note that stations contain measurements:
*
* <ul>
* <li>
* both stations and measurements feature types are mapped and published in the
* same isolated workspace
* </li>
* <li>
* stations feature type is published in the isolated workspace and measurement type
* is an included type (i.e. it is not published)
* </li>
* <li>
* only stations feature type is published in the isolated workspace and the global
* (non isolated) measurements feature type is used for feature chaining
* </li>
* </ul>
*
* All mappings can be used for GML 3.1 and GML 3.2 with the correct parameterization.
*/
public final class IsolatedNamespacesWfsTest extends AbstractAppSchemaTestSupport {

// workspaces isolation first use case GML 3.1 namespaces
private static final String STATIONS_1_PREFIX_GML31 = "st_1_gml31";
private static final String MEASUREMENTS_1_PREFIX_GML31 = "ms_1_gml31";

// workspaces isolation first use case GML 3.2 namespaces
private static final String STATIONS_1_PREFIX_GML32 = "st_1_gml32";
private static final String MEASUREMENTS_1_PREFIX_GML32 = "ms_1_gml32";

// workspaces isolation second use case GML 3.1 namespaces
private static final String STATIONS_2_PREFIX_GML31 = "st_2_gml31";
private static final String MEASUREMENTS_2_PREFIX_GML31 = "ms_2_gml31";

// workspaces isolation second use case GML 3.2 namespaces
private static final String STATIONS_2_PREFIX_GML32 = "st_2_gml32";
private static final String MEASUREMENTS_2_PREFIX_GML32 = "ms_2_gml32";

// xpath engines used to check WFS responses
private XpathEngine WFS11_XPATH_ENGINE;
private XpathEngine WFS20_XPATH_ENGINE;

@Before
public void beforeTest() {
// instantiate WFS 1.1 xpath engine
WFS11_XPATH_ENGINE = StationsMockData.buildXpathEngine(
getTestData().getNamespaces(),
"wfs", "http://www.opengis.net/wfs",
"gml", "http://www.opengis.net/gml");
// instantiate WFS 2.0 xpath engine
WFS20_XPATH_ENGINE = StationsMockData.buildXpathEngine(
getTestData().getNamespaces(),
"ows", "http://www.opengis.net/ows/1.1",
"wfs", "http://www.opengis.net/wfs/2.0",
"gml", "http://www.opengis.net/gml/3.2");
}

@Override
protected StationsMockData createTestData() {
// instantiate our custom complex types
return new MockData();
}

/**
* Helper class that will setup custom complex feature types using the stations data set.
*/
private static final class MockData extends StationsMockData {

@Override
public void addContent() {

// GML 3.1 parameters for files parameterization
Map<String, String> gml31Parameters = new HashMap<>();
gml31Parameters.put("GML_PREFIX", "gml31");
gml31Parameters.put("GML_NAMESPACE", "http://www.opengis.net/gml");
gml31Parameters.put("GML_LOCATION", "http://schemas.opengis.net/gml/3.1.1/base/gml.xsd");
// GML 3.2 parameters for files parameterization
Map<String, String> gml32Parameters = new HashMap<>();
gml32Parameters.put("GML_PREFIX", "gml32");
gml32Parameters.put("GML_NAMESPACE", "http://www.opengis.net/gml/3.2");
gml32Parameters.put("GML_LOCATION", "http://schemas.opengis.net/gml/3.2.1/gml.xsd");

// add first use case namespaces
putIsolatedNamespace(STATIONS_1_PREFIX_GML31, STATIONS_URI_GML31);
putIsolatedNamespace(MEASUREMENTS_1_PREFIX_GML31, MEASUREMENTS_URI_GML31);
putIsolatedNamespace(STATIONS_1_PREFIX_GML32, STATIONS_URI_GML32);
putIsolatedNamespace(MEASUREMENTS_1_PREFIX_GML32, MEASUREMENTS_URI_GML32);
// add first use case features types
gml31Parameters.put("USE_CASE", "1");
gml32Parameters.put("USE_CASE", "1");
addMeasurementFeatureType(MEASUREMENTS_1_PREFIX_GML31, "gml31", "measurements1", "isolatedMappings/measurements1.xml", gml31Parameters);
addStationFeatureType(STATIONS_1_PREFIX_GML31, "gml31", "stations1", "isolatedMappings/stations1.xml", gml31Parameters);
addMeasurementFeatureType(MEASUREMENTS_1_PREFIX_GML32, "gml32", "measurements1", "isolatedMappings/measurements1.xml", gml32Parameters);
addStationFeatureType(STATIONS_1_PREFIX_GML32, "gml32", "stations1", "isolatedMappings/stations1.xml", gml32Parameters);

// add second use case namespaces
putIsolatedNamespace(STATIONS_2_PREFIX_GML31, STATIONS_URI_GML31);
putIsolatedNamespace(MEASUREMENTS_2_PREFIX_GML31, MEASUREMENTS_URI_GML31);
putIsolatedNamespace(STATIONS_2_PREFIX_GML32, STATIONS_URI_GML32);
putIsolatedNamespace(MEASUREMENTS_2_PREFIX_GML32, MEASUREMENTS_URI_GML32);
// add second use case features types
gml31Parameters.put("USE_CASE", "2");
gml32Parameters.put("USE_CASE", "2");
addStationFeatureType(STATIONS_2_PREFIX_GML31, "gml31", "stations2", "isolatedMappings/stations2.xml",
"measurements2", "isolatedMappings/measurements2.xml", gml31Parameters);
addStationFeatureType(STATIONS_2_PREFIX_GML32, "gml32", "stations2", "isolatedMappings/stations2.xml",
"measurements2", "isolatedMappings/measurements2.xml", gml32Parameters);
}
}

@Test
public void testIsolatedWorkspacesWithFirstUseCaseWfs11() {
Document document = getAsDOM("st_1_gml31/wfs?request=GetFeature&version=1.1.0&typename=st_1_gml31:Station_gml31");
checkCount(WFS11_XPATH_ENGINE, document, 1, "/wfs:FeatureCollection/gml:featureMember/" +
"st_1_gml31:Station_gml31[@gml:id='st.1'][st_1_gml31:name='isolated_1_station1']");
checkCount(WFS11_XPATH_ENGINE, document, 1, "/wfs:FeatureCollection/gml:featureMember/" +
"st_1_gml31:Station_gml31[@gml:id='st.1']/st_1_gml31:measurements/ms_1_gml31:Measurement_gml31[ms_1_gml31:name='isolated_1_temperature']");
checkCount(WFS11_XPATH_ENGINE, document, 1, "/wfs:FeatureCollection/gml:featureMember/" +
"st_1_gml31:Station_gml31[@gml:id='st.1']/st_1_gml31:measurements/ms_1_gml31:Measurement_gml31[ms_1_gml31:name='isolated_1_wind']");
checkCount(WFS11_XPATH_ENGINE, document, 1, "/wfs:FeatureCollection/gml:featureMember/" +
"st_1_gml31:Station_gml31[@gml:id='st.1']/st_1_gml31:location/gml:Point[gml:pos='1.0 -1.0']");
// request isolated feature type using global service should fail with feature type unknown
document = getAsDOM("wfs?request=GetFeature&version=1.1.0&typename=st_1_gml31:Station_gml31");
checkCount(WFS11_XPATH_ENGINE, document, 1, "/ows:ExceptionReport/ows:Exception[@exceptionCode='InvalidParameterValue']");
}

@Test
public void testIsolatedWorkspacesWithFirstUseCaseWfs20() {
Document document = getAsDOM("st_1_gml32/wfs?request=GetFeature&version=2.0&typename=st_1_gml32:Station_gml32");
checkCount(WFS20_XPATH_ENGINE, document, 1, "/wfs:FeatureCollection/wfs:member/" +
"st_1_gml32:Station_gml32[@gml:id='st.1'][st_1_gml32:name='isolated_1_station1']");
checkCount(WFS20_XPATH_ENGINE, document, 1, "/wfs:FeatureCollection/wfs:member/" +
"st_1_gml32:Station_gml32[@gml:id='st.1']/st_1_gml32:measurements/ms_1_gml32:Measurement_gml32[ms_1_gml32:name='isolated_1_temperature']");
checkCount(WFS20_XPATH_ENGINE, document, 1, "/wfs:FeatureCollection/wfs:member/" +
"st_1_gml32:Station_gml32[@gml:id='st.1']/st_1_gml32:measurements/ms_1_gml32:Measurement_gml32[ms_1_gml32:name='isolated_1_wind']");
checkCount(WFS20_XPATH_ENGINE, document, 1, "/wfs:FeatureCollection/wfs:member/" +
"st_1_gml32:Station_gml32[@gml:id='st.1']/st_1_gml32:location/gml:Point[gml:pos='1.0 -1.0']");
// request isolated feature type using global service should fail with feature type unknown
document = getAsDOM("wfs?request=GetFeature&version=2.0&typename=st_1_gml32:Station_gml32");
checkCount(WFS20_XPATH_ENGINE, document, 1, "/ows:ExceptionReport/ows:Exception[@exceptionCode='InvalidParameterValue']");
}

@Test
public void testIsolatedWorkspacesWithSecondUseCaseWfs11() {
Document document = getAsDOM("st_2_gml31/wfs?request=GetFeature&version=1.1.0&typename=st_2_gml31:Station_gml31");
checkCount(WFS11_XPATH_ENGINE, document, 1, "/wfs:FeatureCollection/gml:featureMember/" +
"st_2_gml31:Station_gml31[@gml:id='st.1'][st_2_gml31:name='isolated_2_station1']");
checkCount(WFS11_XPATH_ENGINE, document, 1, "/wfs:FeatureCollection/gml:featureMember/" +
"st_2_gml31:Station_gml31[@gml:id='st.1']/st_2_gml31:measurements/ms_2_gml31:Measurement_gml31[ms_2_gml31:name='isolated_2_temperature']");
checkCount(WFS11_XPATH_ENGINE, document, 1, "/wfs:FeatureCollection/gml:featureMember/" +
"st_2_gml31:Station_gml31[@gml:id='st.1']/st_2_gml31:measurements/ms_2_gml31:Measurement_gml31[ms_2_gml31:name='isolated_2_wind']");
checkCount(WFS11_XPATH_ENGINE, document, 1, "/wfs:FeatureCollection/gml:featureMember/" +
"st_2_gml31:Station_gml31[@gml:id='st.1']/st_2_gml31:location/gml:Point[gml:pos='1.0 -1.0']");
// request isolated feature type using global service should fail with feature type unknown
document = getAsDOM("wfs?request=GetFeature&version=1.1.0&typename=st_2_gml31:Station_gml31");
checkCount(WFS11_XPATH_ENGINE, document, 1, "/ows:ExceptionReport/ows:Exception[@exceptionCode='InvalidParameterValue']");
}

@Test
public void testIsolatedWorkspacesWithSecondUseCaseWfs20() {
Document document = getAsDOM("st_2_gml32/wfs?request=GetFeature&version=2.0&typename=st_2_gml32:Station_gml32");
checkCount(WFS20_XPATH_ENGINE, document, 1, "/wfs:FeatureCollection/wfs:member/" +
"st_2_gml32:Station_gml32[@gml:id='st.1'][st_2_gml32:name='isolated_2_station1']");
checkCount(WFS20_XPATH_ENGINE, document, 1, "/wfs:FeatureCollection/wfs:member/" +
"st_2_gml32:Station_gml32[@gml:id='st.1']/st_2_gml32:measurements/ms_2_gml32:Measurement_gml32[ms_2_gml32:name='isolated_2_temperature']");
checkCount(WFS20_XPATH_ENGINE, document, 1, "/wfs:FeatureCollection/wfs:member/" +
"st_2_gml32:Station_gml32[@gml:id='st.1']/st_2_gml32:measurements/ms_2_gml32:Measurement_gml32[ms_2_gml32:name='isolated_2_wind']");
checkCount(WFS20_XPATH_ENGINE, document, 1, "/wfs:FeatureCollection/wfs:member/" +
"st_2_gml32:Station_gml32[@gml:id='st.1']/st_2_gml32:location/gml:Point[gml:pos='1.0 -1.0']");
// request isolated feature type using global service should fail with feature type unknown
document = getAsDOM("wfs?request=GetFeature&version=2.0&typename=st_2_gml32:Station_gml32");
checkCount(WFS20_XPATH_ENGINE, document, 1, "/ows:ExceptionReport/ows:Exception[@exceptionCode='InvalidParameterValue']");
}

/**
* Helper method that evaluates a xpath and checks if the number of nodes found
* correspond to the expected number,
*/
private void checkCount(XpathEngine xpathEngine, Document document, int expectedCount, String xpath) {
try {
// evaluate the xpath and compare the number of nodes found
assertThat(xpathEngine.getMatchingNodes(xpath, document).getLength(), is(expectedCount));
} catch (Exception exception) {
throw new RuntimeException("Error evaluating xpath.", exception);
}
}
}

0 comments on commit aedef5e

Please sign in to comment.