From 42c3094d9e0a2387180071380c2b55de5454103a Mon Sep 17 00:00:00 2001 From: Oscar Fonts Date: Sat, 12 May 2012 22:26:49 +0200 Subject: [PATCH 1/4] Publish collections of shapefiles. Datastore management. --- .../rest/GeoServerRESTPublisher.java | 115 ++++++++++-- .../geoserver/rest/decoder/RESTDataStore.java | 41 ++++- .../datastore/GSAbstractDatastoreEncoder.java | 146 ++++++++++++++++ ...DirectoryOfShapefilesDatastoreEncoder.java | 68 ++++++++ .../GSPostGISDatastoreEncoder.java | 2 +- .../GSShapefileDatastoreEncoder.java | 163 ++++++++++++++++++ .../encoder/utils/NestedElementEncoder.java | 2 +- ...rverRESTCreateReadUpdateDatastoreTest.java | 131 ++++++++++++++ .../GeoserverRESTPostgisDatastoreTest.java | 2 +- ...GeoserverRESTPublishShpCollectionTest.java | 84 +++++++++ .../publisher/GeoserverRESTShapeTest.java | 3 +- .../multipleshapefiles/boundaries.dbf | Bin 0 -> 231 bytes .../multipleshapefiles/boundaries.prj | 1 + .../multipleshapefiles/boundaries.shp | Bin 0 -> 6712 bytes .../testdata/multipleshapefiles/cities.dbf | Bin 0 -> 346277 bytes .../testdata/multipleshapefiles/cities.prj | Bin 0 -> 142 bytes .../testdata/multipleshapefiles/cities.shp | Bin 0 -> 35576 bytes src/test/resources/testdata/multipleshp.zip | Bin 0 -> 6056 bytes .../resources/testdata/shapefile/cities.shx | Bin 10236 -> 0 bytes 19 files changed, 739 insertions(+), 19 deletions(-) create mode 100644 src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSAbstractDatastoreEncoder.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSDirectoryOfShapefilesDatastoreEncoder.java rename src/main/java/it/geosolutions/geoserver/rest/encoder/{ => datastore}/GSPostGISDatastoreEncoder.java (99%) create mode 100644 src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSShapefileDatastoreEncoder.java create mode 100644 src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTCreateReadUpdateDatastoreTest.java create mode 100644 src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPublishShpCollectionTest.java create mode 100644 src/test/resources/testdata/multipleshapefiles/boundaries.dbf create mode 100644 src/test/resources/testdata/multipleshapefiles/boundaries.prj create mode 100644 src/test/resources/testdata/multipleshapefiles/boundaries.shp create mode 100644 src/test/resources/testdata/multipleshapefiles/cities.dbf create mode 100644 src/test/resources/testdata/multipleshapefiles/cities.prj create mode 100644 src/test/resources/testdata/multipleshapefiles/cities.shp create mode 100644 src/test/resources/testdata/multipleshp.zip delete mode 100644 src/test/resources/testdata/shapefile/cities.shx diff --git a/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java b/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java index d827dd2..2a709b7 100644 --- a/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java +++ b/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java @@ -30,11 +30,12 @@ import it.geosolutions.geoserver.rest.decoder.utils.NameLinkElem; import it.geosolutions.geoserver.rest.encoder.GSBackupEncoder; import it.geosolutions.geoserver.rest.encoder.GSLayerEncoder; import it.geosolutions.geoserver.rest.encoder.GSNamespaceEncoder; -import it.geosolutions.geoserver.rest.encoder.GSPostGISDatastoreEncoder; import it.geosolutions.geoserver.rest.encoder.GSResourceEncoder; import it.geosolutions.geoserver.rest.encoder.GSResourceEncoder.ProjectionPolicy; import it.geosolutions.geoserver.rest.encoder.GSWorkspaceEncoder; import it.geosolutions.geoserver.rest.encoder.coverage.GSCoverageEncoder; +import it.geosolutions.geoserver.rest.encoder.datastore.GSAbstractDatastoreEncoder; +import it.geosolutions.geoserver.rest.encoder.datastore.GSPostGISDatastoreEncoder; import it.geosolutions.geoserver.rest.encoder.feature.GSFeatureTypeEncoder; import java.io.File; @@ -655,20 +656,20 @@ public class GeoServerRESTPublisher { } /** - * The file, url, and external endpoints are used to specify the method that - * is used to upload the file. - * - * The file method is used to directly upload a file from a local source. + * The {@code file}, {@code url}, and {@code external} endpoints are used to specify + * the method that is used to upload the file. + * * @author Carlo Cancellieri - carlo.cancellieri@geo-solutions.it * */ @@ -677,7 +678,7 @@ public class GeoServerRESTPublisher { } // ========================================================================== - // === DATASTORE + // === DATASTORES // ========================================================================== /** @@ -702,11 +703,52 @@ public class GeoServerRESTPublisher { String result = HTTPUtils.postXml(sUrl, xml, gsuser, gspass); return result != null; } + + /** + * Create a datastore (any datastore extending GSAbstractDatastoreEncoder). + * + * @param workspace + * Name of the workspace to contain the datastore. This will also + * be the prefix of any layer names contained in the datastore. + * @param datastore + * the set of parameters to be set to the datastore (including + * connection parameters). + * @return true if the datastore has been successfully + * created, false otherwise + */ + public boolean createDatastore(String workspace, + GSAbstractDatastoreEncoder datastore) { + String sUrl = restURL + "/rest/workspaces/" + workspace + + "/datastores/"; + String xml = datastore.toString(); + String result = HTTPUtils.postXml(sUrl, xml, gsuser, gspass); + return result != null; + } + + /** + * Update a datastore (any datastore extending GSAbstractDatastoreEncoder). + * + * @param workspace + * Name of the workspace that contains the datastore. + * @param datastore + * the set of parameters to be set to the datastore (including + * connection parameters). + * @return true if the datastore has been successfully + * updated, false otherwise + */ + public boolean updateDatastore(String workspace, + GSAbstractDatastoreEncoder datastore) { + String sUrl = restURL + "/rest/workspaces/" + workspace + + "/datastores/" + datastore.getName(); + String xml = datastore.toString(); + String result = HTTPUtils.putXml(sUrl, xml, gsuser, gspass); + return result != null; + } // ========================================================================== // === SHAPEFILES // ========================================================================== - + /** * Publish a zipped shapefile.
* The defaultCRS will be set to EPSG:4326. @@ -896,6 +938,57 @@ public class GeoServerRESTPublisher { return publishShp(workspace, storename, params, layername, UploadMethod.file, zipFile.toURI(), srs, ProjectionPolicy.NONE,null); } + + + /** + * Publish a collection of shapefiles. + * + * It will automatically create the store and publish each shapefile as a layer. + * + * @param workspace the name of the workspace to use + * @param storeName the name of the store to create + * @param resource the shapefile collection. It can be: + * @return {@code true} if publication successful. + * @throws FileNotFoundException if the specified zip file does not exist. + */ + public boolean publishShpCollection(String workspace, String storeName, URI resource) + throws FileNotFoundException { + + // Deduce upload method & mime type from resource syntax. + UploadMethod method = null; + String mime = null; + if (resource.getScheme().equals("file") || resource.isAbsolute() == false) { + File f = new File(resource); + if (f.exists() && f.isFile() && f.toString().endsWith(".zip")) { + method = UploadMethod.file; + mime = "application/zip"; + } else if (f.isDirectory()) { + method = UploadMethod.external; + mime = "text/plain"; + } + } else { + try { + if(resource.toURL() != null) { + method = UploadMethod.url; + mime = "text/plain"; + } + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Resource is not recognized as a zip file, or a directory, or a valid URL", e); + } + } + + // Create store, upload data, and publish layers + return createStore( + workspace, DataStoreType.datastores, + storeName, method, + DataStoreExtension.shp, // TODO if GEOS-5113 is accepted, change to DataStoreExtension.shpdir + mime, resource, + ParameterConfigure.ALL, + new NameValuePair[0]); + } /** * @param workspace @@ -1159,7 +1252,7 @@ public class GeoServerRESTPublisher { * */ public enum DataStoreExtension { - shp, properties, h2, spatialite + shp, /*shpdir,*/ properties, h2, spatialite // TODO uncomment if GEOS-5113 is accepted } /** diff --git a/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTDataStore.java b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTDataStore.java index 22c24f0..52904d7 100644 --- a/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTDataStore.java +++ b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTDataStore.java @@ -26,7 +26,11 @@ package it.geosolutions.geoserver.rest.decoder; import it.geosolutions.geoserver.rest.decoder.utils.JDOMBuilder; + +import java.util.HashMap; import java.util.List; +import java.util.Map; + import org.jdom.Element; /** @@ -108,15 +112,44 @@ public class RESTDataStore { public String getName() { return dsElem.getChildText("name"); } + + public String getStoreType() { + return dsElem.getChildText("type"); + } + + public String getDescription() { + return dsElem.getChildText("description"); + } + public boolean isEnabled() { + return Boolean.parseBoolean(dsElem.getChildText("enabled")); + } + public String getWorkspaceName() { return dsElem.getChild("workspace").getChildText("name"); } - - protected String getConnectionParameter(String paramName) { + + public Map getConnectionParameters() { Element elConnparm = dsElem.getChild("connectionParameters"); if (elConnparm != null) { - for (Element entry : (List) elConnparm.getChildren("entry")) { + @SuppressWarnings("unchecked") + List elements = (List)elConnparm.getChildren("entry"); + Map params = new HashMap(elements.size()); + for (Element element : elements) { + String key = element.getAttributeValue("key"); + String value = element.getTextTrim(); + params.put(key, value); + } + return params; + } + return null; + } + + @SuppressWarnings("unchecked") + protected String getConnectionParameter(String paramName) { + Element elConnparm = dsElem.getChild("connectionParameters"); + if (elConnparm != null) { + for (Element entry : (List) elConnparm.getChildren("entry")) { String key = entry.getAttributeValue("key"); if (paramName.equals(key)) { return entry.getTextTrim(); @@ -126,7 +159,7 @@ public class RESTDataStore { return null; } - + public DBType getType() { return DBType.get(getConnectionParameter("dbtype")); } diff --git a/src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSAbstractDatastoreEncoder.java b/src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSAbstractDatastoreEncoder.java new file mode 100644 index 0000000..f957810 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSAbstractDatastoreEncoder.java @@ -0,0 +1,146 @@ +/* + * GeoServer-Manager - Simple Manager Library for GeoServer + * + * Copyright (C) 2007,2012 GeoSolutions S.A.S. + * http://www.geo-solutions.it + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package it.geosolutions.geoserver.rest.encoder.datastore; + +import java.util.Map; + +import it.geosolutions.geoserver.rest.decoder.RESTDataStore; +import it.geosolutions.geoserver.rest.encoder.utils.ElementUtils; +import it.geosolutions.geoserver.rest.encoder.utils.NestedElementEncoder; +import it.geosolutions.geoserver.rest.encoder.utils.PropertyXMLEncoder; + +/** + * Generic Datastore encoder. + * + * Provides getters and setters for parameters common to all Datastores, + * an internal placeholder for specific connection parameters, and + * a constructor to read parameters from a {@link RESTDataStore}. + * + * @author Oscar Fonts + */ +public abstract class GSAbstractDatastoreEncoder extends PropertyXMLEncoder { + + final static String ROOT = "dataStore"; + + NestedElementEncoder connectionParameters = new NestedElementEncoder("connectionParameters"); + + GSAbstractDatastoreEncoder(String storeName) { + super(ROOT); + // Add mandatory parameter + ensureValidName(storeName); + setName(storeName); + + // Add connection parameters + addContent(connectionParameters.getRoot()); + } + + /** + * Create a {@value #TYPE} datastore encoder from a store read from server. + * + * @param store The existing store. + * @throws IllegalArgumentException if store type or mandatory parameters are not valid + */ + GSAbstractDatastoreEncoder(RESTDataStore store) { + this(store.getName()); + + // Match datastore type + ensureValidType(store.getStoreType()); + setType(store.getStoreType()); + + // Copy store parameters + setDescription(store.getDescription()); + setEnabled(store.isEnabled()); + + // Copy connection parameters - bulk + Map params = store.getConnectionParameters(); + for(String key : params.keySet()) { + connectionParameters.set(key, params.get(key)); + } + } + + void setType(String type) { + set("type", type); + } + + public String getType() { + return ElementUtils.contains(getRoot(), "type").getTextTrim(); + } + + public void setName(String name) { + ensureValidName(name); + set("name", name); + } + + public String getName() { + return ElementUtils.contains(getRoot(), "name").getTextTrim(); + } + + public void setDescription(String description) { + set("description", description); + } + + public String getDescription() { + return ElementUtils.contains(getRoot(), "description").getTextTrim(); + } + + public void setEnabled(boolean enabled) { + set("enabled", Boolean.toString(enabled)); + } + + public boolean getEnabled() { + return Boolean.parseBoolean(ElementUtils.contains(getRoot(), "enabled").getTextTrim()); + } + + /** + * Check name validity. + * + * @param name the name + * @throws IllegalArgumentException if name is null or empty + */ + void ensureValidName(String name) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException( + "Store name cannot be null or empty"); + } + } + + /** + * Check type validity. + * + * @param type the type. + * @throws IllegalArgumentException if type is not {@value #TYPE} + */ + void ensureValidType(String type) { + if (!type.equals(getValidType())) { + throw new IllegalArgumentException( + "The store type '"+ type +"' is not valid"); + } + } + + /** + * The type of the implementing datastore. + */ + abstract String getValidType(); +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSDirectoryOfShapefilesDatastoreEncoder.java b/src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSDirectoryOfShapefilesDatastoreEncoder.java new file mode 100644 index 0000000..eae422e --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSDirectoryOfShapefilesDatastoreEncoder.java @@ -0,0 +1,68 @@ +/* + * GeoServer-Manager - Simple Manager Library for GeoServer + * + * Copyright (C) 2007,2012 GeoSolutions S.A.S. + * http://www.geo-solutions.it + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package it.geosolutions.geoserver.rest.encoder.datastore; + +import it.geosolutions.geoserver.rest.decoder.RESTDataStore; + +import java.net.URL; + +/** + * Encoder for a {@value #TYPE} datastore. + * + * @author Oscar Fonts + */ +public class GSDirectoryOfShapefilesDatastoreEncoder extends GSShapefileDatastoreEncoder { + + static final String TYPE = "Directory of spatial files (shapefiles)"; + + /** + * Create a {@value #TYPE} datastore with default connection parameters, + * given a store name and a url (the store location). + * + * @param name New datastore name + * @param url The shapefile location in the server, relative to $GEOSERVER_DATA_DIR. + */ + public GSDirectoryOfShapefilesDatastoreEncoder(String name, URL url) { + super(name, url); + setType(TYPE); + } + + /** + * Create a {@value #TYPE} datastore encoder from an existing store read from server. + * + * @param store The existing store. + * @throws IllegalArgumentException if store type or mandatory parameters are not valid + */ + public GSDirectoryOfShapefilesDatastoreEncoder(RESTDataStore store) { + super(store); + } + + /** + * @return {@value #TYPE} + */ + String getValidType() { + return TYPE; + } +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/encoder/GSPostGISDatastoreEncoder.java b/src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSPostGISDatastoreEncoder.java similarity index 99% rename from src/main/java/it/geosolutions/geoserver/rest/encoder/GSPostGISDatastoreEncoder.java rename to src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSPostGISDatastoreEncoder.java index 1db6613..0323fb3 100644 --- a/src/main/java/it/geosolutions/geoserver/rest/encoder/GSPostGISDatastoreEncoder.java +++ b/src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSPostGISDatastoreEncoder.java @@ -23,7 +23,7 @@ * THE SOFTWARE. */ -package it.geosolutions.geoserver.rest.encoder; +package it.geosolutions.geoserver.rest.encoder.datastore; import it.geosolutions.geoserver.rest.encoder.utils.NestedElementEncoder; import it.geosolutions.geoserver.rest.encoder.utils.PropertyXMLEncoder; diff --git a/src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSShapefileDatastoreEncoder.java b/src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSShapefileDatastoreEncoder.java new file mode 100644 index 0000000..3a9d949 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSShapefileDatastoreEncoder.java @@ -0,0 +1,163 @@ +/* + * GeoServer-Manager - Simple Manager Library for GeoServer + * + * Copyright (C) 2007,2012 GeoSolutions S.A.S. + * http://www.geo-solutions.it + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package it.geosolutions.geoserver.rest.encoder.datastore; + +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; + +import it.geosolutions.geoserver.rest.decoder.RESTDataStore; +import it.geosolutions.geoserver.rest.encoder.utils.ElementUtils; + +/** + * Encoder for a {@value #TYPE} datastore. + * + * @author Oscar Fonts + */ +public class GSShapefileDatastoreEncoder extends GSAbstractDatastoreEncoder { + + static final String TYPE = "Shapefile"; + + final static boolean DEFAULT_ENABLED = true; + final static String DEFAULT_CHARSET = "ISO-8859-1"; + final static boolean DEFAULT_CREATE_SPATIAL_INDEX = true; + final static boolean DEFAULT_MEMORY_MAPPED_BUFFER = false; + final static boolean DEFAULT_CACHE_AND_REUSE_MEMORY_MAPS = true; + + /** + * Create a {@value #TYPE} datastore with default connection parameters, + * given a store name and a url (the store location). + * + * The following default connection parameters are set: + *
    + *
  • enabled: {@value #DEFAULT_ENABLED} + *
  • charset: {@value #DEFAULT_CHARSET} + *
  • create spatial index: {@value #DEFAULT_CREATE_SPATIAL_INDEX} + *
  • memory mapped buffer: {@value #DEFAULT_MEMORY_MAPPED_BUFFER} + *
  • cache and reuse memory maps: {@value #DEFAULT_CACHE_AND_REUSE_MEMORY_MAPS} + *
+ * + * @param name New datastore name + * @param url The shapefile location in the server, relative to $GEOSERVER_DATA_DIR. + */ + public GSShapefileDatastoreEncoder(String name, URL url) { + // Set fixed values + super(name); + setType(TYPE); + + // Set mandatory parameter + ensureValidURL(url); + setUrl(url); + + // Set default values + setEnabled(DEFAULT_ENABLED); + setCharset(Charset.forName(DEFAULT_CHARSET)); + setCreateSpatialIndex(DEFAULT_CREATE_SPATIAL_INDEX); + setMemoryMappedBuffer(DEFAULT_MEMORY_MAPPED_BUFFER); + setCacheAndReuseMemoryMaps(DEFAULT_CACHE_AND_REUSE_MEMORY_MAPS); + } + + /** + * Create a {@value #TYPE} datastore encoder from an existing store read from server. + * + * @param store The existing store. + * @throws IllegalArgumentException if store type or mandatory parameters are not valid + */ + public GSShapefileDatastoreEncoder(RESTDataStore store) { + super(store); + + // Check mandatory parameter validity + try { + ensureValidURL(new URL(store.getConnectionParameters().get("url"))); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Shapefile store URL is malformed", e); + } + } + + public void setUrl(URL url) { + ensureValidURL(url); + connectionParameters.set("url", url.toString()); + } + + public URL getUrl() { + try { + return new URL(ElementUtils.contains(connectionParameters.getRoot(), "description").getTextTrim()); + } catch (MalformedURLException e) { + return null; + } + } + + public void setCharset(Charset charset) { + connectionParameters.set("charset", charset.name()); + } + + public Charset getCharset() { + return Charset.forName(ElementUtils.contains(connectionParameters.getRoot(), "charset").getTextTrim()); + } + + public void setCreateSpatialIndex(boolean createSpatialIndex) { + connectionParameters.set("create spatial index", Boolean.toString(createSpatialIndex)); + } + + public boolean getCreateSpatialIndex() { + return Boolean.parseBoolean(ElementUtils.contains(connectionParameters.getRoot(), "create spatial index").getTextTrim()); + } + + public void setMemoryMappedBuffer(boolean memoryMappedBuffer) { + connectionParameters.set("memory mapped buffer", Boolean.toString(memoryMappedBuffer)); + } + + public boolean getMemoryMappedBuffer() { + return Boolean.parseBoolean(ElementUtils.contains(connectionParameters.getRoot(), "memory mapped buffer").getTextTrim()); + } + + public void setCacheAndReuseMemoryMaps(boolean cacheAndReuseMemoryMaps) { + connectionParameters.set("cache and reuse memory maps", Boolean.toString(cacheAndReuseMemoryMaps)); + } + + public boolean getCacheAndReuseMemoryMaps() { + return Boolean.parseBoolean(ElementUtils.contains(connectionParameters.getRoot(), "cache and reuse memory maps").getTextTrim()); + } + + /** + * Check url validity. + * + * @param url the url + * @throws IllegalArgumentException if url is null or empty + */ + private static void ensureValidURL(URL url) { + if (url == null || url.toString().isEmpty()) { + throw new IllegalArgumentException( + "Shapefile store URL cannot be null or empty"); + } + } + + /** + * @return {@value #TYPE} + */ + String getValidType() { + return TYPE; + } +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/encoder/utils/NestedElementEncoder.java b/src/main/java/it/geosolutions/geoserver/rest/encoder/utils/NestedElementEncoder.java index aaf97d8..a72dee4 100644 --- a/src/main/java/it/geosolutions/geoserver/rest/encoder/utils/NestedElementEncoder.java +++ b/src/main/java/it/geosolutions/geoserver/rest/encoder/utils/NestedElementEncoder.java @@ -155,7 +155,7 @@ public class NestedElementEncoder extends XmlElement { // if some previous similar object is found final Element search; if ((search = ElementUtils.contains(getRoot(), new NestedElementFilter( - getRoot(), key, value))) != null) { + getRoot(), key, null))) != null) { // remove it ElementUtils.remove(getRoot(), search); } diff --git a/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTCreateReadUpdateDatastoreTest.java b/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTCreateReadUpdateDatastoreTest.java new file mode 100644 index 0000000..d56a885 --- /dev/null +++ b/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTCreateReadUpdateDatastoreTest.java @@ -0,0 +1,131 @@ +/* + * GeoServer-Manager - Simple Manager Library for GeoServer + * + * Copyright (C) 2007,2012 GeoSolutions S.A.S. + * http://www.geo-solutions.it + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package it.geosolutions.geoserver.rest.publisher; + +import org.junit.Test; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.Map; + +import it.geosolutions.geoserver.rest.GeoServerRESTPublisher; +import it.geosolutions.geoserver.rest.GeoserverRESTTest; +import it.geosolutions.geoserver.rest.decoder.RESTDataStore; +import it.geosolutions.geoserver.rest.encoder.datastore.GSAbstractDatastoreEncoder; +import it.geosolutions.geoserver.rest.encoder.datastore.GSShapefileDatastoreEncoder; +import it.geosolutions.geoserver.rest.encoder.datastore.GSDirectoryOfShapefilesDatastoreEncoder; + +/** + * Test datastore handling (create, read and update): + * + *
  • Tests all the constructors and setters from + * {@link GSDirectoryOfShapefilesDatastoreEncoder} and parent classes + * ({@link GSShapefileDatastoreEncoder}, {@link GSAbstractDatastoreEncoder}). + * + *
  • Tests constructors and getters from {@link RESTDataStore} (reader). + * + *
  • Tests {@link GeoServerRESTPublisher#createDatastore} and + * {@link GeoServerRESTPublisher#updateDatastore} methods.
+ * + *

The sequence is: + *

    + *
  1. Create a DirectoryOfShapefilesDatastoreEncoder, with default parameters. + *
  2. Publish via createDatastore. + *
  3. Read the datastore from server. + *
  4. Test all parameter values. + *
  5. Create a new Encoder from it. + *
  6. Change all datastore parameter to non-default ones. + *
  7. Update via updateDatastore. + *
  8. Read again. + *
  9. Test all new values. + *
+ * + * @author Oscar Fonts + */ +public class GeoserverRESTCreateReadUpdateDatastoreTest extends GeoserverRESTTest { + + private static final String WS_NAME = DEFAULT_WS; + private static final String DS_NAME = "testCreateDatastore"; + private static final String DS_DESCRIPTION = "A description"; + private static URL LOCATION_1; + private static URL LOCATION_2; + + public GeoserverRESTCreateReadUpdateDatastoreTest(String testName) throws Exception { + super(testName); + LOCATION_1 = new URL("file:data/1"); + LOCATION_2 = new URL("file:data/2"); + } + + @Test + public void test() throws Exception { + assertTrue(enabled()); + + // Delete all resources except styles + deleteAllWorkspacesRecursively(); + + // Create workspace + assertTrue(publisher.createWorkspace(WS_NAME)); + + // Create a directory of spatial files with default parameters + GSDirectoryOfShapefilesDatastoreEncoder create = new GSDirectoryOfShapefilesDatastoreEncoder(DS_NAME, LOCATION_1); + assertTrue(publisher.createDatastore(WS_NAME, create)); + + // Read the store from server; check all parameter values + RESTDataStore read = reader.getDatastore(WS_NAME, DS_NAME); + assertEquals(read.getName(), DS_NAME); + assertEquals(read.getWorkspaceName(), WS_NAME); + assertEquals(read.isEnabled(), true); + + Map connParams = read.getConnectionParameters(); + assertEquals(connParams.get("url"), LOCATION_1.toString()); + assertEquals(connParams.get("charset"), "ISO-8859-1"); + assertEquals(connParams.get("create spatial index"), "true"); + assertEquals(connParams.get("memory mapped buffer"), "false"); + assertEquals(connParams.get("cache and reuse memory maps"), "true"); + + // Change all parameter to non-default values + GSDirectoryOfShapefilesDatastoreEncoder update = new GSDirectoryOfShapefilesDatastoreEncoder(read); + update.setDescription(DS_DESCRIPTION); + update.setEnabled(false); + update.setUrl(LOCATION_2); + update.setCharset(Charset.forName("UTF-8")); + update.setCreateSpatialIndex(false); + update.setMemoryMappedBuffer(true); + update.setCacheAndReuseMemoryMaps(false); + + //update the store + assertTrue(publisher.updateDatastore(WS_NAME, update)); + + // Read again, check that all parameters have changed + read = reader.getDatastore(WS_NAME, DS_NAME); + assertEquals(read.getWorkspaceName(), WS_NAME); + assertEquals(read.isEnabled(), false); + connParams = read.getConnectionParameters(); + assertEquals(connParams.get("url"), LOCATION_2.toString()); + assertEquals(connParams.get("charset"), "UTF-8"); + assertEquals(connParams.get("create spatial index"), "false"); + assertEquals(connParams.get("memory mapped buffer"), "true"); + assertEquals(connParams.get("cache and reuse memory maps"), "false"); + } +} diff --git a/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPostgisDatastoreTest.java b/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPostgisDatastoreTest.java index 2d76af5..2bceb7b 100644 --- a/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPostgisDatastoreTest.java +++ b/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPostgisDatastoreTest.java @@ -28,7 +28,7 @@ package it.geosolutions.geoserver.rest.publisher; import it.geosolutions.geoserver.rest.GeoserverRESTTest; import it.geosolutions.geoserver.rest.decoder.RESTDataStore; -import it.geosolutions.geoserver.rest.encoder.GSPostGISDatastoreEncoder; +import it.geosolutions.geoserver.rest.encoder.datastore.GSPostGISDatastoreEncoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPublishShpCollectionTest.java b/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPublishShpCollectionTest.java new file mode 100644 index 0000000..c5bd0a2 --- /dev/null +++ b/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPublishShpCollectionTest.java @@ -0,0 +1,84 @@ +/* + * GeoServer-Manager - Simple Manager Library for GeoServer + * + * Copyright (C) 2007,2012 GeoSolutions S.A.S. + * http://www.geo-solutions.it + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package it.geosolutions.geoserver.rest.publisher; + +import org.junit.Test; +import java.net.URI; +import java.util.List; +import org.springframework.core.io.ClassPathResource; + +import it.geosolutions.geoserver.rest.GeoserverRESTTest; + +/** + * @author Oscar Fonts + */ +public class GeoserverRESTPublishShpCollectionTest extends GeoserverRESTTest { + + final String workspace = DEFAULT_WS; + final String storeName = "testshpcollection"; + + public GeoserverRESTPublishShpCollectionTest(String testName) { + super(testName); + } + + @Test + public void testLocalZip() throws Exception { + assertTrue(enabled()); + + URI location = new ClassPathResource("testdata/multipleshp.zip").getFile().toURI(); + test(location); + } + + @Test + public void testExternalDir() throws Exception { + assertTrue(enabled()); + + URI location = new ClassPathResource("testdata/multipleshapefiles").getFile().toURI(); + test(location); + } + + void test(URI location) throws Exception { + + // Delete all resources except styles + deleteAllWorkspacesRecursively(); + + // Create workspace + assertTrue(publisher.createWorkspace(workspace)); + + // Publish shp collection + assertTrue(publisher.publishShpCollection(workspace, storeName, location)); + + // Test store type */ + /* TODO uncomment if GEOS-5113 is accepted + String storeType = reader.getDatastore(workspace, storeName).getStoreType(); + assertEquals(storeType, "Directory of spatial files (shapefiles)"); + */ + + // Test published layer names + List layers = reader.getLayers().getNames(); + assertTrue(layers.contains("cities")); + assertTrue(layers.contains("boundaries")); + } +} diff --git a/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTShapeTest.java b/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTShapeTest.java index 8859d92..3f3ebfa 100644 --- a/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTShapeTest.java +++ b/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTShapeTest.java @@ -166,11 +166,12 @@ public class GeoserverRESTShapeTest extends GeoserverRESTTest { String ns = "geosolutions"; String storeName = "resttestshp"; String layerName = "cities"; + final String styleName = "restteststyle"; File zipFile = new ClassPathResource("testdata/resttestshp.zip").getFile(); publisher.removeDatastore(DEFAULT_WS, storeName,true); + publisher.removeStyle(styleName); - final String styleName = "restteststyle"; File sldFile = new ClassPathResource("testdata/restteststyle.sld").getFile(); // insert style diff --git a/src/test/resources/testdata/multipleshapefiles/boundaries.dbf b/src/test/resources/testdata/multipleshapefiles/boundaries.dbf new file mode 100644 index 0000000000000000000000000000000000000000..b245a5e9dc6e573aa624707fbec2e76a3a2d2c33 GIT binary patch literal 231 zcmZS1Wa4FHU|?9t;0h!$L2!s;2vo)yB+G%O#MwX8FC-`uBnp<-K$i~<3W^Vj3;>z| zmKVS#@9ggalV?Gb=T!(vEY3~L%S?1EEiNfa%*jktzz-1WT=H{r5{n!|T@<7Mn4%sO literal 0 HcmV?d00001 diff --git a/src/test/resources/testdata/multipleshapefiles/boundaries.prj b/src/test/resources/testdata/multipleshapefiles/boundaries.prj new file mode 100644 index 0000000..7a70628 --- /dev/null +++ b/src/test/resources/testdata/multipleshapefiles/boundaries.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] diff --git a/src/test/resources/testdata/multipleshapefiles/boundaries.shp b/src/test/resources/testdata/multipleshapefiles/boundaries.shp new file mode 100644 index 0000000000000000000000000000000000000000..85957fa133cd66248fb23a2d35c27883cfb6390d GIT binary patch literal 6712 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 literal 0 HcmV?d00001 diff --git a/src/test/resources/testdata/multipleshapefiles/cities.dbf b/src/test/resources/testdata/multipleshapefiles/cities.dbf new file mode 100644 index 0000000000000000000000000000000000000000..6b53516a6bb0cef4831bdf50b19f2df846fe3a76 GIT binary patch literal 346277 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 VLvs^TV@lDk>JFSO7r; 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 cj2PhIuGTA}IqZ}e-*0c$#nJP}BgVGe~3L+&T z2+|ZHMLL9<&^rVYk__+6ntA=+`R4yTXYKWzv(CECx}Saaugg%6?$miY8k*BI1^gd0 zT-T0e?PzFd@F&H3Qja}exjQ+$eCiC9a(e9I*yl9o>|}3m-Zj$`$Z{$|DO|%j=hi94 ztme;%4|Y0+SFCO6Y}DniURA$x9nC2&9jHG&hNTz51dCwG&Lx@WWM`yjE}2_eKTOF; z&ioJvx-`_VfM2-6r5xUPRfm?w@L!^^hH!lFIf)SWzoU4*bcK5Fdwa9SnF83iXE$26 z!_&A#?x{${3BE)1-#Z;NLjxSSgfQTCl9h9WGHW_Q`GxeD)r5?pLPCzla`U6+xq+7q z?9AS}6AFfgnsImEv3(X`@F@cm;n8KoEZU%%gI!Rlt4F>$$exj zl@LJPCs6ZroW}L{st{^kSwnXU9vos?OB!c&J}U~jUQ0z*;B>zAT~Nb2<*`}d$D|uB z2^LT(#%xiIU?RA&QA0#OY=b6VA+N^Adi!vesg)y~fNLbBIK0eLK6k4e@AqQCP(Sps zh$nnrr><9kPj>TJWe#04zK`~Imj#**;0#BL1x?|?Z+f=>bb1Gq*g}noT#J)a8nB+t z`g}DFQh=#^D3{T}HV?Fck}4K5**i@_UZJD?^ixI3=Xl(xq(+us-1!LcPF|(AQibqP zIqj@qlP}1KW4UxDzcKW+cybh+Z?8AT?K5NL(RxS)^eo1@VX}j-lR`mJ%_P*EdFg!b zezzM($5+#1E_x;$7DUY2sfgn9 zHIvB`J5zwy#p&S6nF~JBtF~RC zPy#jjzK3vK1e+digSSjIaJM~{vbPu`s3&qoR7@0*%`mLfLDQLTmYi3OjV0Q?%v6_H zYV&>eK#GHLwdRVTwsO}+rMIXDu5yvWVs!3N!JZUjyMsnN!9^FcQE`=1X=&#$+ z6*sCBMv!1?cs8kA0$gxpm3UMMPEUZ7ZY-Mq!i$U~{_O)aRpABC#rF@(%$MwN@3#Cz1UDR# zQwB|qNbrPn$q&Y_&?~)OJM3DC_i5?VYT-Ft>7=cR%|C~qGc+<$#he#;E#6Ct+3%5q zN>|}{oYP+(%}6W7H&rgZ0$`Lj??wNhS8uY9fmvU+u@e*Q$WqMY&{ZAmAf~-1IO;Am zi|u&Lq!{^s+5Z)UP_RwXXD~Hgn&4XgG&0ygB;TBcqsb&NhM{|(IQDf zJhb6dGao!W`u}!>6Xo<`+3v;$VlXXqLVhs8CzMed3=P#Avx|=JB50L zSo0F|l>_>w?OT&pqJ^D6o|Fxz0!G!hGoM~wJy6`+@Lg5ZwAg9W6MYDflu+g*(fg3g z*u(hyAf*liE(}MqtmM>Uw0LoSv}TZZH(SQGJSDuQ{Z4~!4dCnB>z6th4$E3>7(5~^ z!J)!#bscjg^rz+5{f@on?*z~&muAz35-~mb!p-QcApKxcj!Pf`H0xaX%%`N-gZpv;y?tb(XmZ343#zVRF>O~ z$_BbkzJ+|~U(>wq$+HrTNP1EaTJyhrci&k|`;%y{k;~Ce%*SHwG4pS1jQn@E2O=Qh zI%R8*;yr_r`0bcy)VjpHFurBZU6c4QRJNFAwDdcHUhs%!;;g~0Gj9b1JpwA}8r)Gz za%aCX{f77`+t&PehDNmA`_ex3R-DPk^&|+5_g=0hACL1$;k3a1UVQiF=OQ}-T|i&M z_=~b1UlBQYO+&@7n&&v+Tv*j)%}OZ>QTV0gwAL8h_?mi z{F)FYM~|Y^t>-kpms>qNgMlrrJ21>J$>}WbHhju!%0DXFzgpr;XK+l-{9c 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 From d1352fb470d63c0bc4e57dcb36a32ce7600971cd Mon Sep 17 00:00:00 2001 From: Oscar Fonts Date: Sun, 20 May 2012 12:28:16 +0200 Subject: [PATCH 2/4] Removed references to GEOS-5113, which turned out to be nonsense --- .../geosolutions/geoserver/rest/GeoServerRESTPublisher.java | 4 ++-- .../publisher/GeoserverRESTPublishShpCollectionTest.java | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java b/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java index 2a709b7..50e4dfe 100644 --- a/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java +++ b/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java @@ -984,7 +984,7 @@ public class GeoServerRESTPublisher { return createStore( workspace, DataStoreType.datastores, storeName, method, - DataStoreExtension.shp, // TODO if GEOS-5113 is accepted, change to DataStoreExtension.shpdir + DataStoreExtension.shp, mime, resource, ParameterConfigure.ALL, new NameValuePair[0]); @@ -1252,7 +1252,7 @@ public class GeoServerRESTPublisher { * */ public enum DataStoreExtension { - shp, /*shpdir,*/ properties, h2, spatialite // TODO uncomment if GEOS-5113 is accepted + shp, properties, h2, spatialite } /** diff --git a/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPublishShpCollectionTest.java b/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPublishShpCollectionTest.java index c5bd0a2..e3c5554 100644 --- a/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPublishShpCollectionTest.java +++ b/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPublishShpCollectionTest.java @@ -70,11 +70,8 @@ public class GeoserverRESTPublishShpCollectionTest extends GeoserverRESTTest { // Publish shp collection assertTrue(publisher.publishShpCollection(workspace, storeName, location)); - // Test store type */ - /* TODO uncomment if GEOS-5113 is accepted String storeType = reader.getDatastore(workspace, storeName).getStoreType(); - assertEquals(storeType, "Directory of spatial files (shapefiles)"); - */ + assertEquals(storeType, "Shapefile"); // Test published layer names List layers = reader.getLayers().getNames(); From 876727a6e261552ba3f322ebdb07dc52d3a7513e Mon Sep 17 00:00:00 2001 From: Oscar Fonts Date: Mon, 21 May 2012 17:51:55 +0200 Subject: [PATCH 3/4] Keep both PostGISEncoders, old one marked as deprecated --- .../rest/GeoServerRESTPublisher.java | 8 +- .../encoder/GSPostGISDatastoreEncoder.java | 260 ++++++++++++++++++ .../datastore/GSPostGISDatastoreEncoder.java | 182 +++--------- .../GeoserverRESTPostgisDatastoreTest.java | 2 +- 4 files changed, 302 insertions(+), 150 deletions(-) create mode 100644 src/main/java/it/geosolutions/geoserver/rest/encoder/GSPostGISDatastoreEncoder.java diff --git a/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java b/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java index 50e4dfe..67e24eb 100644 --- a/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java +++ b/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java @@ -35,7 +35,7 @@ import it.geosolutions.geoserver.rest.encoder.GSResourceEncoder.ProjectionPolicy import it.geosolutions.geoserver.rest.encoder.GSWorkspaceEncoder; import it.geosolutions.geoserver.rest.encoder.coverage.GSCoverageEncoder; import it.geosolutions.geoserver.rest.encoder.datastore.GSAbstractDatastoreEncoder; -import it.geosolutions.geoserver.rest.encoder.datastore.GSPostGISDatastoreEncoder; +import it.geosolutions.geoserver.rest.encoder.GSPostGISDatastoreEncoder; import it.geosolutions.geoserver.rest.encoder.feature.GSFeatureTypeEncoder; import java.io.File; @@ -694,6 +694,8 @@ public class GeoServerRESTPublisher { * * @return true if the PostGIS datastore has been successfully * created, false otherwise + * @deprecated Will be deleted in next version 1.5.x. + * Use {@link #createDatastore(String, GSAbstractDatastoreEncoder)} instead. */ public boolean createPostGISDatastore(String workspace, GSPostGISDatastoreEncoder datastoreEncoder) { @@ -705,7 +707,7 @@ public class GeoServerRESTPublisher { } /** - * Create a datastore (any datastore extending GSAbstractDatastoreEncoder). + * Create a datastore (any datastore extending {@link GSAbstractDatastoreEncoder}). * * @param workspace * Name of the workspace to contain the datastore. This will also @@ -726,7 +728,7 @@ public class GeoServerRESTPublisher { } /** - * Update a datastore (any datastore extending GSAbstractDatastoreEncoder). + * Update a datastore (any datastore extending {@link GSAbstractDatastoreEncoder}). * * @param workspace * Name of the workspace that contains the datastore. diff --git a/src/main/java/it/geosolutions/geoserver/rest/encoder/GSPostGISDatastoreEncoder.java b/src/main/java/it/geosolutions/geoserver/rest/encoder/GSPostGISDatastoreEncoder.java new file mode 100644 index 0000000..0d85ff0 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/encoder/GSPostGISDatastoreEncoder.java @@ -0,0 +1,260 @@ +/* + * GeoServer-Manager - Simple Manager Library for GeoServer + * + * Copyright (C) 2007,2011 GeoSolutions S.A.S. + * http://www.geo-solutions.it + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package it.geosolutions.geoserver.rest.encoder; + +import it.geosolutions.geoserver.rest.encoder.utils.NestedElementEncoder; +import it.geosolutions.geoserver.rest.encoder.utils.PropertyXMLEncoder; + +/** + * Geoserver datastore XML encoder. + * + * @author Eric Grosso + * @author ETj + * @author Carlo Cancellieri - carlo.cancellieri@geo-solutions.it + * + * @deprecated Will be removed in next version 1.5.x. + * Use {@link it.geosolutions.geoserver.rest.encoder.datastore.GSPostGISDatastoreEncoder} instead. + */ +public class GSPostGISDatastoreEncoder extends PropertyXMLEncoder { + + private NestedElementEncoder connectionParameters = new NestedElementEncoder("connectionParameters"); + + public GSPostGISDatastoreEncoder() { + super("dataStore"); + addContent(connectionParameters.getRoot()); + + addType("PostGIS"); // may be overwritten with e.g. "PostGIS (JNDI)" + addDatabaseType("postgis"); + } + + /** + * Set some initial defaults. + *

+ * The default parameters are as follows:
    + *
  • maximum connections: 10,
  • + *
  • minimum connections: 1,
  • + *
  • fetch size: 1000,
  • + *
  • connection timeout: 20 seconds,
  • + *
  • loose BBox: true,
  • + *
  • prepared statements: false,
  • + *
  • maximum open prepared statements: 50.
  • + *
+ */ + public void defaultInit() { + setMinConnections(1); + setMaxConnections(10); + setFetchSize(1000); + setConnectionTimeout(20); + setLooseBBox(true); + setPreparedStatements(false); + setMaxOpenPreparedStatements(50); + } + + protected void addName(String name) { + add("name", name); + } + + public void setName(String name) { + set("name", name); + } + + protected void addDescription(String description) { + add("description", description); + } + + public void setDescription(String description) { + set("description", description); + } + + protected void addType(String type) { + add("type", type); + } + + public void setType(String type) { + set("type", type); + } + + protected void addEnabled(boolean enabled) { + add("enabled", Boolean.toString(enabled)); + } + + public void setEnabled(boolean enabled) { + set("enabled", Boolean.toString(enabled)); + } + + protected void addNamespace(String namespace) { + connectionParameters.add("namespace", namespace); + } + + public void setNamespace(String namespace) { + connectionParameters.set("namespace", namespace); + } + + protected void addHost(String host) { + connectionParameters.add("host", host); + } + + public void setHost(String host) { + connectionParameters.set("host", host); + } + + protected void addPort(int port) { + connectionParameters.add("port", Integer.toString(port)); + } + + public void setPort(int port) { + connectionParameters.set("port", Integer.toString(port)); + } + + protected void addDatabase(String database) { + connectionParameters.add("database", database); + } + + public void setDatabase(String database) { + connectionParameters.set("database", database); + } + + protected void addSchema(String schema) { + connectionParameters.add("schema", schema); + } + + public void setSchema(String schema) { + connectionParameters.set("schema", schema); + } + + protected void addUser(String user) { + connectionParameters.add("user", user); + } + + public void setUser(String user) { + connectionParameters.set("user", user); + } + + protected void addPassword(String password) { + connectionParameters.add("passwd", password); + } + + public void setPassword(String password) { + connectionParameters.set("passwd", password); + } + + protected void addDatabaseType(String dbtype) { + connectionParameters.add("dbtype", dbtype); + } + + public void setDatabaseType(String dbtype) { + connectionParameters.set("dbtype", dbtype); + } + + protected void addJndiReferenceName(String jndiReferenceName) { + connectionParameters.add("jndiReferenceName", jndiReferenceName); + } + + public void setJndiReferenceName(String jndiReferenceName) { + connectionParameters.set("jndiReferenceName", jndiReferenceName); + } + + protected void addExposePrimaryKeys(boolean exposePrimaryKeys) { + connectionParameters.add("Expose primary keys", Boolean.toString(exposePrimaryKeys)); + } + + public void setExposePrimaryKeys(boolean exposePrimaryKeys) { + connectionParameters.set("Expose primary keys", Boolean.toString(exposePrimaryKeys)); + } + + protected void addMaxConnections(int maxConnections) { + connectionParameters.add("max connections", Integer.toString(maxConnections)); + } + + public void setMaxConnections(int maxConnections) { + connectionParameters.set("max connections", Integer.toString(maxConnections)); + } + + protected void addMinConnections(int minConnections) { + connectionParameters.add("min connections", Integer.toString(minConnections)); + } + + public void setMinConnections(int minConnections) { + connectionParameters.set("min connections", Integer.toString(minConnections)); + } + + protected void addFetchSize(int fetchSize) { + connectionParameters.add("fetch size", Integer.toString(fetchSize)); + } + + public void setFetchSize(int fetchSize) { + connectionParameters.set("fetch size", Integer.toString(fetchSize)); + } + + protected void addConnectionTimeout(int seconds) { + connectionParameters.add("Connection timeout", Integer.toString(seconds)); + } + + public void setConnectionTimeout(int seconds) { + connectionParameters.set("Connection timeout", Integer.toString(seconds)); + } + + protected void addValidateConnections(boolean validateConnections) { + connectionParameters.add("validate connections", Boolean.toString(validateConnections)); + } + + public void setValidateConnections(boolean validateConnections) { + connectionParameters.set("validate connections", Boolean.toString(validateConnections)); + } + + protected void addPrimaryKeyMetadataTable(String primaryKeyMetadataTable) { + connectionParameters.add("Primary key metadata table", primaryKeyMetadataTable); + } + + public void setPrimaryKeyMetadataTable(String primaryKeyMetadataTable) { + connectionParameters.set("Primary key metadata table", primaryKeyMetadataTable); + } + + protected void addLooseBBox(boolean looseBBox) { + connectionParameters.add("Loose bbox", Boolean.toString(looseBBox)); + } + + public void setLooseBBox(boolean looseBBox) { + connectionParameters.set("Loose bbox", Boolean.toString(looseBBox)); + } + + protected void addPreparedStatements(boolean preparedStatements) { + connectionParameters.add("preparedStatements", Boolean.toString(preparedStatements)); + } + + public void setPreparedStatements(boolean preparedStatements) { + connectionParameters.set("preparedStatements", Boolean.toString(preparedStatements)); + } + + protected void addMaxOpenPreparedStatements(int maxOpenPreparedStatements) { + connectionParameters.add("Max open prepared statements", Integer.toString(maxOpenPreparedStatements)); + } + + public void setMaxOpenPreparedStatements(int maxOpenPreparedStatements) { + connectionParameters.set("Max open prepared statements", Integer.toString(maxOpenPreparedStatements)); + } + +} \ No newline at end of file diff --git a/src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSPostGISDatastoreEncoder.java b/src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSPostGISDatastoreEncoder.java index 0323fb3..19978ee 100644 --- a/src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSPostGISDatastoreEncoder.java +++ b/src/main/java/it/geosolutions/geoserver/rest/encoder/datastore/GSPostGISDatastoreEncoder.java @@ -1,7 +1,7 @@ /* * GeoServer-Manager - Simple Manager Library for GeoServer * - * Copyright (C) 2007,2011 GeoSolutions S.A.S. + * Copyright (C) 2007,2012 GeoSolutions S.A.S. * http://www.geo-solutions.it * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -22,236 +22,126 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ - package it.geosolutions.geoserver.rest.encoder.datastore; -import it.geosolutions.geoserver.rest.encoder.utils.NestedElementEncoder; -import it.geosolutions.geoserver.rest.encoder.utils.PropertyXMLEncoder; - /** - * Geoserver datastore XML encoder. + * Encoder for a {@value #TYPE} datastore. * * @author Eric Grosso * @author ETj * @author Carlo Cancellieri - carlo.cancellieri@geo-solutions.it + * @author Oscar Fonts */ -public class GSPostGISDatastoreEncoder extends PropertyXMLEncoder { +public class GSPostGISDatastoreEncoder extends GSAbstractDatastoreEncoder { - private NestedElementEncoder connectionParameters = new NestedElementEncoder("connectionParameters"); + static final String TYPE = "PostGIS"; - public GSPostGISDatastoreEncoder() { - super("dataStore"); - addContent(connectionParameters.getRoot()); + static final int DEFAULT_MIN_CONNECTIONS = 1; + static final int DEFAULT_MAX_CONNECTIONS = 10; + static final int DEFAULT_FETCH_SIZE = 1000; + static final int DEFAULT_CONNECTION_TIMEOUT = 20; + static final boolean DEFAULT_LOOSE_BBOX = true; + static final boolean DEFAULT_PREPARED_STATEMENTS = false; + static final int DEFAULT_MAX_OPEN_PREPARED_STATEMENTS = 50; + + public GSPostGISDatastoreEncoder(String name) { + super(name); + + // Set mandatory parameter + setType(TYPE); + setDatabaseType("postgis"); + + // Set default values + setMinConnections(DEFAULT_MIN_CONNECTIONS); + setMaxConnections(DEFAULT_MAX_CONNECTIONS); + setFetchSize(DEFAULT_FETCH_SIZE); + setConnectionTimeout(DEFAULT_CONNECTION_TIMEOUT); + setLooseBBox(DEFAULT_LOOSE_BBOX); + setPreparedStatements(DEFAULT_PREPARED_STATEMENTS); + setMaxOpenPreparedStatements(DEFAULT_MAX_OPEN_PREPARED_STATEMENTS); - addType("PostGIS"); // may be overwritten with e.g. "PostGIS (JNDI)" - addDatabaseType("postgis"); - } - - /** - * Set some initial defaults. - *

- * The default parameters are as follows:
    - *
  • maximum connections: 10,
  • - *
  • minimum connections: 1,
  • - *
  • fetch size: 1000,
  • - *
  • connection timeout: 20 seconds,
  • - *
  • loose BBox: true,
  • - *
  • prepared statements: false,
  • - *
  • maximum open prepared statements: 50.
  • - *
- */ - public void defaultInit() { - setMinConnections(1); - setMaxConnections(10); - setFetchSize(1000); - setConnectionTimeout(20); - setLooseBBox(true); - setPreparedStatements(false); - setMaxOpenPreparedStatements(50); - } - - protected void addName(String name) { - add("name", name); - } - - public void setName(String name) { - set("name", name); - } - - protected void addDescription(String description) { - add("description", description); - } - - public void setDescription(String description) { - set("description", description); - } - - protected void addType(String type) { - add("type", type); - } - - public void setType(String type) { - set("type", type); - } - - protected void addEnabled(boolean enabled) { - add("enabled", Boolean.toString(enabled)); - } - - public void setEnabled(boolean enabled) { - set("enabled", Boolean.toString(enabled)); - } - - protected void addNamespace(String namespace) { - connectionParameters.add("namespace", namespace); } public void setNamespace(String namespace) { connectionParameters.set("namespace", namespace); } - protected void addHost(String host) { - connectionParameters.add("host", host); - } - public void setHost(String host) { connectionParameters.set("host", host); } - - protected void addPort(int port) { - connectionParameters.add("port", Integer.toString(port)); - } public void setPort(int port) { connectionParameters.set("port", Integer.toString(port)); } - - protected void addDatabase(String database) { - connectionParameters.add("database", database); - } public void setDatabase(String database) { connectionParameters.set("database", database); } - protected void addSchema(String schema) { - connectionParameters.add("schema", schema); - } - public void setSchema(String schema) { connectionParameters.set("schema", schema); } - - protected void addUser(String user) { - connectionParameters.add("user", user); - } public void setUser(String user) { connectionParameters.set("user", user); } - - protected void addPassword(String password) { - connectionParameters.add("passwd", password); - } public void setPassword(String password) { connectionParameters.set("passwd", password); } - protected void addDatabaseType(String dbtype) { - connectionParameters.add("dbtype", dbtype); - } - public void setDatabaseType(String dbtype) { connectionParameters.set("dbtype", dbtype); } - - protected void addJndiReferenceName(String jndiReferenceName) { - connectionParameters.add("jndiReferenceName", jndiReferenceName); - } public void setJndiReferenceName(String jndiReferenceName) { connectionParameters.set("jndiReferenceName", jndiReferenceName); } - - protected void addExposePrimaryKeys(boolean exposePrimaryKeys) { - connectionParameters.add("Expose primary keys", Boolean.toString(exposePrimaryKeys)); - } public void setExposePrimaryKeys(boolean exposePrimaryKeys) { connectionParameters.set("Expose primary keys", Boolean.toString(exposePrimaryKeys)); } - protected void addMaxConnections(int maxConnections) { - connectionParameters.add("max connections", Integer.toString(maxConnections)); - } - public void setMaxConnections(int maxConnections) { connectionParameters.set("max connections", Integer.toString(maxConnections)); } - protected void addMinConnections(int minConnections) { - connectionParameters.add("min connections", Integer.toString(minConnections)); - } - public void setMinConnections(int minConnections) { connectionParameters.set("min connections", Integer.toString(minConnections)); } - protected void addFetchSize(int fetchSize) { - connectionParameters.add("fetch size", Integer.toString(fetchSize)); - } - public void setFetchSize(int fetchSize) { connectionParameters.set("fetch size", Integer.toString(fetchSize)); } - protected void addConnectionTimeout(int seconds) { - connectionParameters.add("Connection timeout", Integer.toString(seconds)); - } - public void setConnectionTimeout(int seconds) { connectionParameters.set("Connection timeout", Integer.toString(seconds)); } - protected void addValidateConnections(boolean validateConnections) { - connectionParameters.add("validate connections", Boolean.toString(validateConnections)); - } - public void setValidateConnections(boolean validateConnections) { connectionParameters.set("validate connections", Boolean.toString(validateConnections)); } - protected void addPrimaryKeyMetadataTable(String primaryKeyMetadataTable) { - connectionParameters.add("Primary key metadata table", primaryKeyMetadataTable); - } - public void setPrimaryKeyMetadataTable(String primaryKeyMetadataTable) { connectionParameters.set("Primary key metadata table", primaryKeyMetadataTable); } - protected void addLooseBBox(boolean looseBBox) { - connectionParameters.add("Loose bbox", Boolean.toString(looseBBox)); - } - public void setLooseBBox(boolean looseBBox) { connectionParameters.set("Loose bbox", Boolean.toString(looseBBox)); } - protected void addPreparedStatements(boolean preparedStatements) { - connectionParameters.add("preparedStatements", Boolean.toString(preparedStatements)); - } - public void setPreparedStatements(boolean preparedStatements) { connectionParameters.set("preparedStatements", Boolean.toString(preparedStatements)); } - protected void addMaxOpenPreparedStatements(int maxOpenPreparedStatements) { - connectionParameters.add("Max open prepared statements", Integer.toString(maxOpenPreparedStatements)); - } - public void setMaxOpenPreparedStatements(int maxOpenPreparedStatements) { connectionParameters.set("Max open prepared statements", Integer.toString(maxOpenPreparedStatements)); } - -} \ No newline at end of file + + /** + * @return {@value #TYPE} + */ + String getValidType() { + return TYPE; + } +} diff --git a/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPostgisDatastoreTest.java b/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPostgisDatastoreTest.java index 2bceb7b..2d76af5 100644 --- a/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPostgisDatastoreTest.java +++ b/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPostgisDatastoreTest.java @@ -28,7 +28,7 @@ package it.geosolutions.geoserver.rest.publisher; import it.geosolutions.geoserver.rest.GeoserverRESTTest; import it.geosolutions.geoserver.rest.decoder.RESTDataStore; -import it.geosolutions.geoserver.rest.encoder.datastore.GSPostGISDatastoreEncoder; +import it.geosolutions.geoserver.rest.encoder.GSPostGISDatastoreEncoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; From 15341547f17d0b4bffd3d73b1b0c95e72cf3a2ce Mon Sep 17 00:00:00 2001 From: Oscar Fonts Date: Thu, 24 May 2012 12:50:42 +0200 Subject: [PATCH 4/4] Moved Datastore management to new architecture, using GeoServerRest[Foo]Managers --- .../geoserver/rest/GeoServerRESTManager.java | 82 +++++++++++++++++ .../rest/GeoServerRESTPublisher.java | 44 +-------- .../geoserver/rest/HTTPUtils.java | 2 +- .../manager/GeoServerRESTAbstractManager.java | 55 +++++++++++ .../GeoServerRESTDatastoreManager.java | 92 +++++++++++++++++++ .../GeoserverRESTDatastoreManagerTest.java} | 30 +++--- ...GeoserverRESTPublishShpCollectionTest.java | 8 +- 7 files changed, 255 insertions(+), 58 deletions(-) create mode 100644 src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTManager.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/manager/GeoServerRESTAbstractManager.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/manager/GeoServerRESTDatastoreManager.java rename src/test/java/it/geosolutions/geoserver/rest/{publisher/GeoserverRESTCreateReadUpdateDatastoreTest.java => manager/GeoserverRESTDatastoreManagerTest.java} (86%) diff --git a/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTManager.java b/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTManager.java new file mode 100644 index 0000000..1d4fc32 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTManager.java @@ -0,0 +1,82 @@ +/* + * GeoServer-Manager - Simple Manager Library for GeoServer + * + * Copyright (C) 2007,2012 GeoSolutions S.A.S. + * http://www.geo-solutions.it + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package it.geosolutions.geoserver.rest; + +import it.geosolutions.geoserver.rest.manager.GeoServerRESTAbstractManager; +import it.geosolutions.geoserver.rest.manager.GeoServerRESTDatastoreManager; + +import java.net.URL; + +/** + * The single entry point to all of geoserver-manager functionality. + * + * Instance this one, and use getters to use different components. These are: + *
    + *
  • getReader() simple, high-level access methods. + *
  • getPublisher() simple, high-level pubhish methods. + *
  • getFooManager, full-fledged management of catalog objects. + *
+ * @author Oscar Fonts + */ +public class GeoServerRESTManager extends GeoServerRESTAbstractManager { + + private final GeoServerRESTPublisher publisher; + private final GeoServerRESTReader reader; + + private final GeoServerRESTDatastoreManager datastoreManager; + + /** + * Default constructor. + * + * Indicates connection parameters to remote GeoServer instance. + * + * @param restURL GeoServer REST API endpoint + * @param username GeoServer REST API authorized username + * @param password GeoServer REST API password for the former username + */ + public GeoServerRESTManager(URL restURL, String username, String password) { + super(restURL, username, password); + + // Internal publisher and reader, provide simple access methods. + publisher = new GeoServerRESTPublisher(restURL.toString(), username, password); + reader = new GeoServerRESTReader(restURL, username, password); + + // Classes for fine-grained management of catalog components. + datastoreManager = new GeoServerRESTDatastoreManager(restURL, username, password); + } + + public GeoServerRESTPublisher getPublisher() { + return publisher; + } + + public GeoServerRESTReader getReader() { + return reader; + } + + public GeoServerRESTDatastoreManager getDatastoreManager() { + return datastoreManager; + } + +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java b/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java index 67e24eb..d2b0d87 100644 --- a/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java +++ b/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java @@ -34,7 +34,6 @@ import it.geosolutions.geoserver.rest.encoder.GSResourceEncoder; import it.geosolutions.geoserver.rest.encoder.GSResourceEncoder.ProjectionPolicy; import it.geosolutions.geoserver.rest.encoder.GSWorkspaceEncoder; import it.geosolutions.geoserver.rest.encoder.coverage.GSCoverageEncoder; -import it.geosolutions.geoserver.rest.encoder.datastore.GSAbstractDatastoreEncoder; import it.geosolutions.geoserver.rest.encoder.GSPostGISDatastoreEncoder; import it.geosolutions.geoserver.rest.encoder.feature.GSFeatureTypeEncoder; @@ -695,7 +694,7 @@ public class GeoServerRESTPublisher { * @return true if the PostGIS datastore has been successfully * created, false otherwise * @deprecated Will be deleted in next version 1.5.x. - * Use {@link #createDatastore(String, GSAbstractDatastoreEncoder)} instead. + * Use {@link GeoServerRESTDatastoreManager} instead. */ public boolean createPostGISDatastore(String workspace, GSPostGISDatastoreEncoder datastoreEncoder) { @@ -705,47 +704,6 @@ public class GeoServerRESTPublisher { String result = HTTPUtils.postXml(sUrl, xml, gsuser, gspass); return result != null; } - - /** - * Create a datastore (any datastore extending {@link GSAbstractDatastoreEncoder}). - * - * @param workspace - * Name of the workspace to contain the datastore. This will also - * be the prefix of any layer names contained in the datastore. - * @param datastore - * the set of parameters to be set to the datastore (including - * connection parameters). - * @return true if the datastore has been successfully - * created, false otherwise - */ - public boolean createDatastore(String workspace, - GSAbstractDatastoreEncoder datastore) { - String sUrl = restURL + "/rest/workspaces/" + workspace - + "/datastores/"; - String xml = datastore.toString(); - String result = HTTPUtils.postXml(sUrl, xml, gsuser, gspass); - return result != null; - } - - /** - * Update a datastore (any datastore extending {@link GSAbstractDatastoreEncoder}). - * - * @param workspace - * Name of the workspace that contains the datastore. - * @param datastore - * the set of parameters to be set to the datastore (including - * connection parameters). - * @return true if the datastore has been successfully - * updated, false otherwise - */ - public boolean updateDatastore(String workspace, - GSAbstractDatastoreEncoder datastore) { - String sUrl = restURL + "/rest/workspaces/" + workspace - + "/datastores/" + datastore.getName(); - String xml = datastore.toString(); - String result = HTTPUtils.putXml(sUrl, xml, gsuser, gspass); - return result != null; - } // ========================================================================== // === SHAPEFILES diff --git a/src/main/java/it/geosolutions/geoserver/rest/HTTPUtils.java b/src/main/java/it/geosolutions/geoserver/rest/HTTPUtils.java index 143418a..dd7d527 100644 --- a/src/main/java/it/geosolutions/geoserver/rest/HTTPUtils.java +++ b/src/main/java/it/geosolutions/geoserver/rest/HTTPUtils.java @@ -54,7 +54,7 @@ import org.slf4j.LoggerFactory; /** * Low level HTTP utilities. */ -class HTTPUtils { +public class HTTPUtils { private static final Logger LOGGER = LoggerFactory.getLogger(HTTPUtils.class); /** diff --git a/src/main/java/it/geosolutions/geoserver/rest/manager/GeoServerRESTAbstractManager.java b/src/main/java/it/geosolutions/geoserver/rest/manager/GeoServerRESTAbstractManager.java new file mode 100644 index 0000000..6fc3464 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/manager/GeoServerRESTAbstractManager.java @@ -0,0 +1,55 @@ +/* + * GeoServer-Manager - Simple Manager Library for GeoServer + * + * Copyright (C) 2007,2012 GeoSolutions S.A.S. + * http://www.geo-solutions.it + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package it.geosolutions.geoserver.rest.manager; + +import java.net.URL; + +/** + * Abstract manager, common functionality and interface + * for all GeoServerRESTFooManager classes. + * + * @author Oscar Fonts + */ +public abstract class GeoServerRESTAbstractManager { + + protected final URL restURL; + protected final String gsuser; + protected final String gspass; + + /** + * Default constructor. + * + * Indicates connection parameters to remote GeoServer instance. + * + * @param restURL GeoServer REST API endpoint + * @param username GeoServer REST API authorized username + * @param password GeoServer REST API password for the former username + */ + public GeoServerRESTAbstractManager(URL restURL, String username, String password) { + this.restURL = restURL; + this.gsuser = username; + this.gspass = password; + } +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/manager/GeoServerRESTDatastoreManager.java b/src/main/java/it/geosolutions/geoserver/rest/manager/GeoServerRESTDatastoreManager.java new file mode 100644 index 0000000..36572be --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/manager/GeoServerRESTDatastoreManager.java @@ -0,0 +1,92 @@ +/* + * GeoServer-Manager - Simple Manager Library for GeoServer + * + * Copyright (C) 2007,2012 GeoSolutions S.A.S. + * http://www.geo-solutions.it + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package it.geosolutions.geoserver.rest.manager; + +import it.geosolutions.geoserver.rest.HTTPUtils; +import it.geosolutions.geoserver.rest.encoder.datastore.GSAbstractDatastoreEncoder; + +import java.net.URL; + +/** + * Manage datastores. + * + * To pass connection parameters, use the encoders derived from + * {@link GSAbstractDatastoreEncoder}. + * + * @author Oscar Fonts + */ +public class GeoServerRESTDatastoreManager extends GeoServerRESTAbstractManager { + + /** + * Default constructor. + * + * @param restURL GeoServer REST API endpoint + * @param username GeoServer REST API authorized username + * @param password GeoServer REST API password for the former username + */ + public GeoServerRESTDatastoreManager(URL restURL, String username, String password) { + super(restURL, username, password); + } + + /** + * Create a datastore. + * + * @param workspace + * Name of the workspace to contain the datastore. This will also + * be the prefix of any layer names contained in the datastore. + * @param datastore + * the set of parameters to be set to the datastore (including + * connection parameters). + * @return true if the datastore has been successfully + * created, false otherwise + */ + + public boolean create(String workspace, GSAbstractDatastoreEncoder datastore) { + String sUrl = restURL + "/rest/workspaces/" + workspace + + "/datastores/"; + String xml = datastore.toString(); + String result = HTTPUtils.postXml(sUrl, xml, gsuser, gspass); + return result != null; + } + + /** + * Update a datastore. + * + * @param workspace + * Name of the workspace that contains the datastore. + * @param datastore + * the set of parameters to be set to the datastore (including + * connection parameters). + * @return true if the datastore has been successfully + * updated, false otherwise + */ + public boolean update(String workspace, GSAbstractDatastoreEncoder datastore) { + String sUrl = restURL + "/rest/workspaces/" + workspace + + "/datastores/" + datastore.getName(); + String xml = datastore.toString(); + String result = HTTPUtils.putXml(sUrl, xml, gsuser, gspass); + return result != null; + } +} diff --git a/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTCreateReadUpdateDatastoreTest.java b/src/test/java/it/geosolutions/geoserver/rest/manager/GeoserverRESTDatastoreManagerTest.java similarity index 86% rename from src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTCreateReadUpdateDatastoreTest.java rename to src/test/java/it/geosolutions/geoserver/rest/manager/GeoserverRESTDatastoreManagerTest.java index d56a885..98af5f6 100644 --- a/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTCreateReadUpdateDatastoreTest.java +++ b/src/test/java/it/geosolutions/geoserver/rest/manager/GeoserverRESTDatastoreManagerTest.java @@ -22,14 +22,15 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package it.geosolutions.geoserver.rest.publisher; +package it.geosolutions.geoserver.rest.manager; import org.junit.Test; + import java.net.URL; import java.nio.charset.Charset; import java.util.Map; -import it.geosolutions.geoserver.rest.GeoServerRESTPublisher; +import it.geosolutions.geoserver.rest.GeoServerRESTManager; import it.geosolutions.geoserver.rest.GeoserverRESTTest; import it.geosolutions.geoserver.rest.decoder.RESTDataStore; import it.geosolutions.geoserver.rest.encoder.datastore.GSAbstractDatastoreEncoder; @@ -45,41 +46,46 @@ import it.geosolutions.geoserver.rest.encoder.datastore.GSDirectoryOfShapefilesD * *
  • Tests constructors and getters from {@link RESTDataStore} (reader). * - *
  • Tests {@link GeoServerRESTPublisher#createDatastore} and - * {@link GeoServerRESTPublisher#updateDatastore} methods. + *
  • Tests {@link GeoServerRESTDatastoreManager} create and update methods. * *

    The sequence is: *

      *
    1. Create a DirectoryOfShapefilesDatastoreEncoder, with default parameters. - *
    2. Publish via createDatastore. + *
    3. Publish via GeoServerRESTDatastoreManager.create. *
    4. Read the datastore from server. *
    5. Test all parameter values. *
    6. Create a new Encoder from it. *
    7. Change all datastore parameter to non-default ones. - *
    8. Update via updateDatastore. + *
    9. Update via GeoServerRESTDatastoreManager.update. *
    10. Read again. *
    11. Test all new values. *
    * * @author Oscar Fonts */ -public class GeoserverRESTCreateReadUpdateDatastoreTest extends GeoserverRESTTest { - +public class GeoserverRESTDatastoreManagerTest extends GeoserverRESTTest { + + public final GeoServerRESTManager manager; + private static final String WS_NAME = DEFAULT_WS; private static final String DS_NAME = "testCreateDatastore"; private static final String DS_DESCRIPTION = "A description"; private static URL LOCATION_1; private static URL LOCATION_2; - public GeoserverRESTCreateReadUpdateDatastoreTest(String testName) throws Exception { + public GeoserverRESTDatastoreManagerTest(String testName) throws Exception { super(testName); + manager = new GeoServerRESTManager(new URL(RESTURL), RESTUSER, RESTPW); + LOCATION_1 = new URL("file:data/1"); LOCATION_2 = new URL("file:data/2"); } @Test public void test() throws Exception { - assertTrue(enabled()); + if (!enabled()) { + return; + } // Delete all resources except styles deleteAllWorkspacesRecursively(); @@ -89,7 +95,7 @@ public class GeoserverRESTCreateReadUpdateDatastoreTest extends GeoserverRESTTes // Create a directory of spatial files with default parameters GSDirectoryOfShapefilesDatastoreEncoder create = new GSDirectoryOfShapefilesDatastoreEncoder(DS_NAME, LOCATION_1); - assertTrue(publisher.createDatastore(WS_NAME, create)); + assertTrue(manager.getDatastoreManager().create(WS_NAME, create)); // Read the store from server; check all parameter values RESTDataStore read = reader.getDatastore(WS_NAME, DS_NAME); @@ -115,7 +121,7 @@ public class GeoserverRESTCreateReadUpdateDatastoreTest extends GeoserverRESTTes update.setCacheAndReuseMemoryMaps(false); //update the store - assertTrue(publisher.updateDatastore(WS_NAME, update)); + assertTrue(manager.getDatastoreManager().update(WS_NAME, update)); // Read again, check that all parameters have changed read = reader.getDatastore(WS_NAME, DS_NAME); diff --git a/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPublishShpCollectionTest.java b/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPublishShpCollectionTest.java index e3c5554..8463bbe 100644 --- a/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPublishShpCollectionTest.java +++ b/src/test/java/it/geosolutions/geoserver/rest/publisher/GeoserverRESTPublishShpCollectionTest.java @@ -45,7 +45,9 @@ public class GeoserverRESTPublishShpCollectionTest extends GeoserverRESTTest { @Test public void testLocalZip() throws Exception { - assertTrue(enabled()); + if (!enabled()) { + return; + } URI location = new ClassPathResource("testdata/multipleshp.zip").getFile().toURI(); test(location); @@ -53,7 +55,9 @@ public class GeoserverRESTPublishShpCollectionTest extends GeoserverRESTTest { @Test public void testExternalDir() throws Exception { - assertTrue(enabled()); + if (!enabled()) { + return; + } URI location = new ClassPathResource("testdata/multipleshapefiles").getFile().toURI(); test(location);