From ec1120c240a552ec62ae7d298db58e7807522cc7 Mon Sep 17 00:00:00 2001 From: ETj Date: Tue, 10 May 2011 16:05:15 +0200 Subject: [PATCH] Initial revision --- .gitignore | 14 + createGitIgnore | 26 + pom.xml | 231 ++++++ .../rest/GeoServerRESTPublisher.java | 704 ++++++++++++++++++ .../geoserver/rest/GeoServerRESTReader.java | 515 +++++++++++++ .../geoserver/rest/HTTPUtils.java | 430 +++++++++++ .../rest/decoder/RESTAbstractList.java | 121 +++ .../geoserver/rest/decoder/RESTCoverage.java | 240 ++++++ .../rest/decoder/RESTCoverageList.java | 62 ++ .../rest/decoder/RESTCoverageStore.java | 98 +++ .../rest/decoder/RESTCoverageStoreList.java | 61 ++ .../geoserver/rest/decoder/RESTDataStore.java | 133 ++++ .../rest/decoder/RESTDataStoreList.java | 47 ++ .../rest/decoder/RESTFeatureType.java | 246 ++++++ .../geoserver/rest/decoder/RESTLayer.java | 180 +++++ .../rest/decoder/RESTLayerGroup.java | 120 +++ .../rest/decoder/RESTLayerGroupList.java | 67 ++ .../geoserver/rest/decoder/RESTLayerList.java | 63 ++ .../rest/decoder/RESTNamespaceList.java | 133 ++++ .../geoserver/rest/decoder/RESTResource.java | 105 +++ .../geoserver/rest/decoder/RESTStyleList.java | 65 ++ .../rest/decoder/RESTWorkspaceList.java | 128 ++++ .../geoserver/rest/decoder/package.html | 5 + .../rest/decoder/utils/JDOMBuilder.java | 61 ++ .../rest/decoder/utils/JDOMListIterator.java | 57 ++ .../rest/decoder/utils/NameLinkElem.java | 45 ++ .../geoserver/rest/decoder/utils/package.html | 3 + .../rest/encoder/GSCoverageEncoder.java | 42 ++ .../rest/encoder/GSFeatureTypeEncoder.java | 51 ++ .../rest/encoder/GSLayerEncoder.java | 47 ++ .../rest/encoder/GSWorkspaceEncoder.java | 47 ++ .../rest/encoder/PropertyXMLEncoder.java | 101 +++ .../geoserver/rest/encoder/package.html | 5 + .../geosolutions/geoserver/rest/package.html | 3 + .../geoserver/rest/ConfigTest.java | 114 +++ .../rest/GeoserverRESTPublisherTest.java | 334 +++++++++ .../rest/GeoserverRESTReaderTest.java | 203 +++++ .../geoserver/rest/GeoserverRESTTest.java | 257 +++++++ src/test/resources/log4j.properties | 11 + src/test/resources/testdata/default_line.sld | 39 + src/test/resources/testdata/default_point.sld | 45 ++ .../resources/testdata/default_polygon.sld | 43 ++ src/test/resources/testdata/raster.sld | 20 + src/test/resources/testdata/resttestdem.tif | Bin 0 -> 102213 bytes src/test/resources/testdata/resttestshp.zip | Bin 0 -> 39118 bytes src/test/resources/testdata/restteststyle.sld | 84 +++ 46 files changed, 5406 insertions(+) create mode 100644 .gitignore create mode 100755 createGitIgnore create mode 100644 pom.xml create mode 100644 src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTReader.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/HTTPUtils.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/RESTAbstractList.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/RESTCoverage.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/RESTCoverageList.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/RESTCoverageStore.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/RESTCoverageStoreList.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/RESTDataStore.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/RESTDataStoreList.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/RESTFeatureType.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/RESTLayer.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/RESTLayerGroup.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/RESTLayerGroupList.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/RESTLayerList.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/RESTNamespaceList.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/RESTResource.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/RESTStyleList.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/RESTWorkspaceList.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/package.html create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/utils/JDOMBuilder.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/utils/JDOMListIterator.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/utils/NameLinkElem.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/decoder/utils/package.html create mode 100644 src/main/java/it/geosolutions/geoserver/rest/encoder/GSCoverageEncoder.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/encoder/GSFeatureTypeEncoder.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/encoder/GSLayerEncoder.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/encoder/GSWorkspaceEncoder.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/encoder/PropertyXMLEncoder.java create mode 100644 src/main/java/it/geosolutions/geoserver/rest/encoder/package.html create mode 100644 src/main/java/it/geosolutions/geoserver/rest/package.html create mode 100644 src/test/java/it/geosolutions/geoserver/rest/ConfigTest.java create mode 100644 src/test/java/it/geosolutions/geoserver/rest/GeoserverRESTPublisherTest.java create mode 100644 src/test/java/it/geosolutions/geoserver/rest/GeoserverRESTReaderTest.java create mode 100644 src/test/java/it/geosolutions/geoserver/rest/GeoserverRESTTest.java create mode 100644 src/test/resources/log4j.properties create mode 100644 src/test/resources/testdata/default_line.sld create mode 100644 src/test/resources/testdata/default_point.sld create mode 100644 src/test/resources/testdata/default_polygon.sld create mode 100644 src/test/resources/testdata/raster.sld create mode 100644 src/test/resources/testdata/resttestdem.tif create mode 100644 src/test/resources/testdata/resttestshp.zip create mode 100644 src/test/resources/testdata/restteststyle.sld diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79dd63f --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +## +## This file has been automatically generated with the command line: +## for pom in $(find -name "pom.xml") ; do path=${pom%*pom.xml} ; echo '#' $path ; echo ; echo ${path}target ; echo ; done > .gitignore +## +## - ETj + +nb*.xml +.classpath +.project +.settings +# ./ + +./target +target diff --git a/createGitIgnore b/createGitIgnore new file mode 100755 index 0000000..614a19d --- /dev/null +++ b/createGitIgnore @@ -0,0 +1,26 @@ +OUTFILE=gitignore + +cat >$OUTFILE < .gitignore +## +## - ETj + +nb*.xml +.classpath +.project +.settings +EOF + +for pom in $(find -name "pom.xml") +do + path=${pom%*pom.xml} + echo '#' $path >>$OUTFILE + echo >>$OUTFILE + echo ${path}target >>$OUTFILE + echo >>$OUTFILE +done + +echo New file gitignore has been created. You may now want to replace your original .gitignore file. +echo diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c166bfb --- /dev/null +++ b/pom.xml @@ -0,0 +1,231 @@ + + + + 4.0.0 + + it.geosolutions + geoserver-manager + 1.1-SNAPSHOT + + jar + + GeoServer 2 Manager - REST based + + GeoServer Manager is a library to interact with GeoServer 2.x. + The scope of this library is to have a simple API, and use as few external + libs as possible. + + + 2007 + + + GeoSolutions + http://www.geosolutions.it + + + + + etj + Emanuele Tajariol + etj@geosolutions.it + GeoSolutions + http://www.geosolutions.it + + architect + developer + + +1 + + + + + + MIT License + http://opensource.org/licenses/mit-license.php + repo + + + + http://code.google.com/p/geoserver-manager/ + + + googlecode + http://code.google.com/p/geoserver-manager/issues/list + + + + + GeoServer Manager User List + geoserver-manager-users@googlegroups.com + http://groups.google.com/group/geoserver-manager-users/topics + + + + + scm:git:https://github.com/geosolutions-it/geoserver-manager.git + + master + https://github.com/geosolutions-it/geoserver-manager + + + + http://maven.geo-solutions.it + + false + geosolutions + ftp://maven.geo-solutions.it/ + + + demo.geosolutions + scp://demo.geosolutions.it/var/www/share/javadoc/gsman + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.0.2 + + 1.5 + 1.5 + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.7 + + + + + + + + + + + true + org.apache.maven.plugins + maven-source-plugin + + true + + + + attach-sources + + jar + + + + + + + + + + + + + + + + org.apache.maven.wagon + wagon-ftp + 1.0-beta-7 + + + + + + + + org.codehaus.mojo + cobertura-maven-plugin + + 2.2 + + + + + + + + commons-io + commons-io + 1.4 + + + + commons-httpclient + commons-httpclient + 3.1 + + + + org.jdom + jdom + 1.1 + + + + log4j + log4j + 1.2.16 + + + + + + + + junit + junit + 4.8.1 + test + + + + org.springframework + spring-core + 2.5.6.SEC02 + test + + + + + diff --git a/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java b/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java new file mode 100644 index 0000000..7367ef8 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTPublisher.java @@ -0,0 +1,704 @@ +/* + * 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; + +import it.geosolutions.geoserver.rest.decoder.RESTCoverageList; +import it.geosolutions.geoserver.rest.decoder.RESTCoverageStore; +import it.geosolutions.geoserver.rest.encoder.GSCoverageEncoder; +import it.geosolutions.geoserver.rest.encoder.GSFeatureTypeEncoder; +import it.geosolutions.geoserver.rest.encoder.GSLayerEncoder; +import it.geosolutions.geoserver.rest.encoder.GSWorkspaceEncoder; + +import java.io.File; +import java.io.FileNotFoundException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; + +import org.apache.log4j.Logger; + +/** + * Connect to a GeoServer instance to publish or modify data. + *

+ * There are no modifiable instance fields, so all the calls are thread-safe. + * + * @author ETj (etj at geo-solutions.it) + */ +public class GeoServerRESTPublisher { + + private static final Logger LOGGER = Logger.getLogger(GeoServerRESTPublisher.class); + private final String restURL; + private final String gsuser; + private final String gspass; + + /** + * Creates a GeoServerRESTPublisher for a given GeoServer instance + * with the given auth credentials. + * + * @param restURL the base GeoServer URL (e.g.: http://localhost:8080/geoserver) + * @param username username auth credential + * @param password password auth credential + */ + public GeoServerRESTPublisher(String restURL, String username, String password) { + this.restURL = restURL; + this.gsuser = username; + this.gspass = password; + } + + + //========================================================================== + //=== WORKSPACES + //========================================================================== + + /** + * Create a new Workspace + * + * @param workspace The name of the new workspace. + * + *

+ * This is the equivalent call with cUrl: + *

{@code curl -u admin:geoserver -XPOST \
+     *      -H 'Content-type: text/xml' \
+     *      -d "$WORKSPACE" \
+     *      http://$GSIP:$GSPORT/$SERVLET/rest/workspaces
+     * }
+ */ + public boolean createWorkspace(String workspace) { + String sUrl = restURL + "/rest/workspaces"; + GSWorkspaceEncoder wsenc = new GSWorkspaceEncoder(workspace); + String wsxml = wsenc.encodeXml(); + String result = HTTPUtils.postXml(sUrl, wsxml, gsuser, gspass); + return result != null; + } + + //========================================================================== + //=== STYLES + //========================================================================== + + /** + * Store and publish an SLD. + *

+ * This is the equivalent call with cUrl: + *

+     * {@code curl -u admin:geoserver -XPOST \
+     *      -H 'Content-type: application/vnd.ogc.sld+xml' \
+     *      -d @$FULLSLD \
+     *      http://$GSIP:$GSPORT/$SERVLET/rest/styles}
+ * + * @param sldBody the SLD document as an XML String. + * + * @return true if the operation completed successfully. + */ + public boolean publishStyle(String sldBody) { + String sUrl = restURL + "/rest/styles"; + String result = HTTPUtils.post(sUrl, sldBody, "application/vnd.ogc.sld+xml", gsuser, gspass); + return result != null; + } + + /** + * Store and publish an SLD. + * + * @param sldFile the File containing the SLD document. + * + * @return true if the operation completed successfully. + */ + public boolean publishStyle(File sldFile) { + return publishStyle(sldFile, null); + } + + /** + * Store and publish an SLD, assigning it a name. + * + * @param sldFile the File containing the SLD document. + * @param name the Style name. + * + * @return true if the operation completed successfully. + */ + public boolean publishStyle(File sldFile, String name) { + String sUrl = restURL + "/rest/styles"; + if(name != null) + sUrl += "?name=" + encode(name); + LOGGER.debug("POSTing new style " + name + " to " + sUrl); + String result = HTTPUtils.post(sUrl, sldFile, "application/vnd.ogc.sld+xml", gsuser, gspass); + return result != null; + } + + /** + * Remove a Style. + *

+ * The Style will be unpublished and the related SLD file will be removed. + * + * @param styleName the name of the Style to remove. + * + * @return true if the operation completed successfully. + */ + public boolean removeStyle(String styleName) { + styleName = styleName.replaceAll(":", "_"); // ??? + styleName = encode(styleName); // spaces may + String sUrl = restURL + "/rest/styles/" + styleName + "?purge=true"; + return HTTPUtils.delete(sUrl, gsuser, gspass); + } + + //========================================================================== + //=== SHAPEFILES + //========================================================================== + + /** + * Publish a zipped shapefile. + *
The CRS will be forced to EPSG:4326. + * + * @param workspace + * @param storename + * @param layername + * @param zipFile + * @return true if the operation completed successfully. + * @throws FileNotFoundException + */ + public boolean publishShp(String workspace, String storename, String layername, File zipFile) throws FileNotFoundException { + return publishShp(workspace, storename, layername, zipFile, "EPSG:4326"); + } + + /** + * Publish a zipped shapefile. + * + * @param workspace + * @param storename + * @param layerName + * @param nativeCrs + * @param defaultStyle may be null + * @return true if the operation completed successfully. + * @throws FileNotFoundException + */ + public boolean publishShp(String workspace, String storename, String layerName, File zipFile, String nativeCrs, String defaultStyle) throws FileNotFoundException { + boolean sent = publishShp(workspace, storename, layerName, zipFile, nativeCrs); + if (sent) { + + try { + GSLayerEncoder layerEncoder = new GSLayerEncoder(); + layerEncoder.setDefaultStyle(defaultStyle); + configureLayer(layerEncoder, layerName); + } catch (Exception e) { + sent = false; + } + } + + return sent; + } + + /** + * Publish a zipped shapefile. + * + *

These are the equivalent calls with cUrl: + *

{@code
+     *curl -u admin:geoserver -XPUT -H 'Content-type: application/zip' \
+     *      --data-binary @$ZIPFILE \
+     *      http://$GSIP:$GSPORT/$SERVLET/rest/workspaces/$WORKSPACE/datastores/$STORENAME/file.shp
+     *
+     *curl -u admin:geoserver -XPOST -H 'Content-type: text/xml'  \
+     *      -d "$BAREEPSG:4326true"  \
+     *      http://$GSIP:$GSPORT/$SERVLET/rest/workspaces/$WORKSPACE/datastores/$STORENAME/featuretypes/$LAYERNAME
+     * }
+ * + * @return true if the operation completed successfully. + */ + public boolean publishShp(String workspace, String storename, String layername, File zipFile, String srs) throws FileNotFoundException { + // build full URL + StringBuilder sbUrl = new StringBuilder(restURL).append("/rest/workspaces/").append(workspace).append("/datastores/").append(storename).append("/file.shp?"); +// if (workspace != null) { +// sbUrl.append("namespace=").append(workspace); +// } +// sbUrl.append("&SRS=4326&SRSHandling=Force"); // hack + + String sentResult = HTTPUtils.put(sbUrl.toString(), zipFile, "application/zip", gsuser, gspass); + boolean shpSent = sentResult != null; + + if (shpSent) { + LOGGER.info("Zipfile successfully uploaded (layer:" + layername + " zip:" + zipFile + ")"); + + StringBuilder postUrl = new StringBuilder(restURL) + .append("/rest/workspaces/").append(workspace) + .append("/datastores/").append(storename) + .append("/featuretypes/").append(layername); + + GSFeatureTypeEncoder fte = new GSFeatureTypeEncoder(); + fte.setName(layername); + fte.setSRS(srs); + + String configuredResult = HTTPUtils.putXml(postUrl.toString(), fte.encodeXml(), this.gsuser, this.gspass); + boolean shpConfigured = configuredResult != null; + + if (!shpConfigured) { + LOGGER.warn("Error in configuring " + workspace + ":" + storename + "/" + layername + " -- Zipfile was uploaded successfully: " + zipFile); + } else { + LOGGER.info("Shapefile successfully configured (layer:" + layername + ")"); + } + + return shpConfigured; + + } else { + LOGGER.warn("Error in sending zipfile " + workspace + ":" + storename + "/" + layername + " " + zipFile); + return false; + } + + } + + /** + * Publish a table in a PostGis store as a new layer. + * + *

This is the equivalent call with cUrl: + *

{@code curl -u admin:geoserver -XPOST -H 'Content-type: text/xml' \
+     *      -d "easia_gaul_1_aggrEPSG:4326true" \
+     *      http://localhost:8080/geoserver/rest/workspaces/it.geosolutions/datastores/pg_kids/featuretypes
+     * }
+ * + * and a PUT to + *
restURL + "/rest/layers/" + layerName + * + */ + public boolean publishDBLayer(String workspace, String storename, String layername, String srs, String defaultStyle) { + StringBuilder postUrl = new StringBuilder(restURL) + .append("/rest/workspaces/").append(workspace) + .append("/datastores/").append(storename) + .append("/featuretypes"); + + GSFeatureTypeEncoder fte = new GSFeatureTypeEncoder(); + fte.setName(layername); + fte.setSRS(srs); // srs=null?"EPSG:4326":srs); + String ftypeXml = fte.encodeXml(); + + String configuredResult = HTTPUtils.postXml(postUrl.toString(), ftypeXml, this.gsuser, this.gspass); + boolean published = configuredResult != null; + boolean configured = false; + + if (!published) { + LOGGER.warn("Error in publishing ("+configuredResult+") " + + workspace + ":" + storename + "/" + layername); + } else { + LOGGER.info("DB layer successfully added (layer:" + layername + ")"); + + GSLayerEncoder layerEncoder = new GSLayerEncoder(); + layerEncoder.setDefaultStyle(defaultStyle); + configured = configureLayer(layerEncoder, layername); + + if (!configured) { + LOGGER.warn("Error in configuring ("+configuredResult+") " + + workspace + ":" + storename + "/" + layername); + } else { + LOGGER.info("DB layer successfully configured (layer:" + layername + ")"); + } + } + + return published && configured; + } + + //========================================================================== + //=== GEOTIFF + //========================================================================== + + /** + * Publish a GeoTiff. + * + *

This is the equivalent call with cUrl: + *

{@code
+     *curl -u admin:geoserver -XPUT -H 'Content-type: text' -d "file:$FULLPATH" \
+     *      http://$GSIP:$GSPORT/$SERVLET/rest/workspaces/$WORKSPACE/coveragestores/$STORENAME/external.geotiff
+     * }
+ * + * @return true if the operation completed successfully. + * @deprecated UNTESTED + */ + public boolean publishGeoTIFF(String workspace, String storeName, File geotiff) throws FileNotFoundException { + String sUrl = restURL + "/rest/workspaces/" + workspace + "/coveragestores/" + storeName + "/geotiff"; + String sendResult = HTTPUtils.put(sUrl, geotiff, "text", gsuser, gspass); // CHECKME: text?!? + boolean sent = sendResult != null; + return sent; + } + + /** + * Publish a GeoTiff already in a filesystem readable by GeoServer. + * + * @param workspace an existing workspace + * @param storeName the coverageStore to be created + * @param geotiff the geoTiff to be published + * + * @return a PublishedCoverage, or null on errors + * @throws FileNotFoundException + */ + public RESTCoverageStore publishExternalGeoTIFF(String workspace, String storeName, File geotiff, String srs, String defaultStyle) throws FileNotFoundException { + // create store + String sUrl = restURL + "/rest/workspaces/" + workspace + "/coveragestores/" + storeName + "/external.geotiff"; + String sendResult = HTTPUtils.put(sUrl, geotiff.toURI().toString(), "text/plain", gsuser, gspass); + RESTCoverageStore store = RESTCoverageStore.build(sendResult); + + if (store!=null) { + try { +// // retrieve coverage name + GeoServerRESTReader reader = new GeoServerRESTReader(restURL, gsuser, gspass); + RESTCoverageList covList = reader.getCoverages(workspace, storeName); + if(covList.isEmpty()) { + LOGGER.error("No coverages found in new coveragestore " + storeName); + return null; + } + String coverageName = covList.get(0).getName(); + + // config coverage props (srs) + GSCoverageEncoder coverageEncoder = new GSCoverageEncoder(); + coverageEncoder.setSRS(srs); + configureCoverage(coverageEncoder, workspace, storeName, coverageName); + + // config layer props (style, ...) + GSLayerEncoder layerEncoder = new GSLayerEncoder(); + layerEncoder.setDefaultStyle(defaultStyle); + configureLayer(layerEncoder, coverageName); + + } catch (Exception e) { + LOGGER.warn("Could not configure external GEOTiff:" + storeName, e); + store = null; // TODO: should we remove the configured pc? + } + } + + return store; + } + + //========================================================================== + //=== MOSAIC + //========================================================================== + + /** + * Publish a Mosaic already in a filesystem readable by GeoServer. + * + *

Sample cUrl usage:
+ * <> + * curl -u admin:geoserver -XPUT -H 'Content-type: text' -d "file:$ABSPORTDIR" + * http://$GSIP:$GSPORT/$SERVLET/rest/workspaces/$WORKSPACE/coveragestores/$BAREDIR/external.imagemosaic + * + * @param workspace an existing workspace + * @param storeName the name of the coverageStore to be created + * @param mosaicDir the directory where the raster images are located + * @return true if the operation completed successfully. + * @throws FileNotFoundException + * + * @deprecated work in progress + */ + public RESTCoverageStore configureExternaMosaicDatastore(String workspace, String storeName, File mosaicDir) throws FileNotFoundException { + if (!mosaicDir.isDirectory()) { + throw new IllegalArgumentException("Not a directory '" + mosaicDir + "'"); + } + String sUrl = restURL + "/rest/workspaces/" + workspace + "/coveragestores/" + storeName + "/external.imagemosaic"; + String sendResult = HTTPUtils.put(sUrl, mosaicDir.toURI().toString(), "text/plain", gsuser, gspass); + return RESTCoverageStore.build(sendResult); + } + + /** + * Publish a Mosaic already in a filesystem readable by GeoServer. + * + *

Sample cUrl usage:
+ * curl -u admin:geoserver -XPUT -H 'Content-type: text' -d "file:$ABSPORTDIR" + * http://$GSIP:$GSPORT/$SERVLET/rest/workspaces/$WORKSPACE/coveragestores/$BAREDIR/external.imagemosaic + * + * @param workspace an existing workspace + * @param storeName the name of the coverageStore to be created + * @param mosaicDir the directory where the raster images are located + * @param defaultStyle may be null + * @return true if the operation completed successfully. + * @throws FileNotFoundException + * + * @deprecated work in progress + */ + public RESTCoverageStore publishExternalMosaic(String workspace, String storeName, File mosaicDir, String srs, String defaultStyle) throws FileNotFoundException { + RESTCoverageStore store = configureExternaMosaicDatastore(workspace, storeName, mosaicDir); + if (store != null ) { + try { +// // retrieve coverage name + GeoServerRESTReader reader = new GeoServerRESTReader(restURL, gsuser, gspass); + RESTCoverageList covList = reader.getCoverages(workspace, storeName); + if(covList.isEmpty()) { + LOGGER.error("No coverages found in new coveragestore " + storeName); + return null; + } + String coverageName = covList.get(0).getName(); + + + // config coverage props (srs) + GSCoverageEncoder coverageEncoder = new GSCoverageEncoder(); + coverageEncoder.setSRS(srs); + configureCoverage(coverageEncoder, workspace, storeName, coverageName); + + + // config layer props (style, ...) + GSLayerEncoder layerEncoder = new GSLayerEncoder(); + layerEncoder.setDefaultStyle(defaultStyle); + configureLayer(layerEncoder, storeName); + + } catch (Exception e) { + LOGGER.warn("Could not configure external mosaic:" + storeName, e); + store = null; // TODO: should we remove the configured store? + } + } + + return store; + } + + //========================================================================== + //=== COVERAGES + //========================================================================== + + /** + * Remove the Coverage configuration from GeoServer. + *
+ * First, the associated layer is removed, then the Coverage configuration itself. + *

+ * CHECKME Maybe the coveragestore has to be removed as well. + * + *

REST URL: + * http://localhost:8080/geoserver/rest/workspaces/it.geosolutions/coveragestores/gbRESTtestStore/coverages/resttestdem.xml + * + * @return true if the operation completed successfully. + */ + public boolean unpublishCoverage(String workspace, String storename, String layername) { + try { + // delete related layer + URL deleteLayerUrl = new URL(restURL + "/rest/layers/" + layername); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Going to delete " + "/rest/layers/" + layername); + } + boolean layerDeleted = HTTPUtils.delete(deleteLayerUrl.toExternalForm(), gsuser, gspass); + if (!layerDeleted) { + LOGGER.warn("Could not delete layer '" + layername + "'"); + return false; + } + // delete the coverage + URL deleteCovUrl = new URL(restURL + "/rest/workspaces/" + workspace + "/coveragestores/" + storename + "/coverages/" + layername); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Going to delete " + "/rest/workspaces/" + workspace + "/coveragestores/" + storename + "/coverages/" + layername); + } + boolean covDeleted = HTTPUtils.delete(deleteCovUrl.toExternalForm(), gsuser, gspass); + if (!covDeleted) { + LOGGER.warn("Could not delete coverage " + workspace + ":" + storename + "/" + layername + ", but layer was deleted."); + } else { + LOGGER.info("Coverage successfully deleted " + workspace + ":" + storename + "/" + layername); + } + return covDeleted; + + // the covstore is still there: should we delete it? + + } catch (MalformedURLException ex) { + LOGGER.error(ex); + return false; + } + } + + //========================================================================== + //=== FEATURETYPES + //========================================================================== + + /** + * Removes the featuretype and the associated layer. + *
You may also want to {@link #removeDatastore(String, String) remove the datastore}. + * + * @return true if the operation completed successfully. + */ + public boolean unpublishFeatureType(String workspace, String storename, String layername) { + try { + // delete related layer + URL deleteLayerUrl = new URL(restURL + "/rest/layers/" + layername); + boolean layerDeleted = HTTPUtils.delete(deleteLayerUrl.toExternalForm(), gsuser, gspass); + if (!layerDeleted) { + LOGGER.warn("Could not delete layer '" + layername + "'"); + return false; + } + // delete the coverage + URL deleteFtUrl = new URL(restURL + "/rest/workspaces/" + workspace + "/datastores/" + storename + "/featuretypes/" + layername); + boolean ftDeleted = HTTPUtils.delete(deleteFtUrl.toExternalForm(), gsuser, gspass); + if (!ftDeleted) { + LOGGER.warn("Could not delete featuretype " + workspace + ":" + storename + "/" + layername + ", but layer was deleted."); + } else { + LOGGER.info("FeatureType successfully deleted " + workspace + ":" + storename + "/" + layername); + } + + return ftDeleted; + + // the store is still there: should we delete it? + + } catch (MalformedURLException ex) { + LOGGER.error(ex); + return false; + } + } + + /** + * Remove a given Datastore in a given Workspace. + * + * @param workspace The name of the workspace + * @param storename The name of the Datastore to remove. + * @return true if the datastore was successfully removed. + */ + public boolean removeDatastore(String workspace, String storename) { + try { + URL deleteStore = new URL(restURL + "/rest/workspaces/" + workspace + "/datastores/" + storename); + boolean deleted = HTTPUtils.delete(deleteStore.toExternalForm(), gsuser, gspass); + if (!deleted) { + LOGGER.warn("Could not delete datastore " + workspace + ":" + storename); + } else { + LOGGER.info("Datastore successfully deleted " + workspace + ":" + storename); + } + + return deleted; + } catch (MalformedURLException ex) { + LOGGER.error(ex); + return false; + } + } + + /** + * Remove a given CoverageStore in a given Workspace. + * + * @param workspace The name of the workspace + * @param storename The name of the CoverageStore to remove. + * @return true if the CoverageStore was successfully removed. + */ + public boolean removeCoverageStore(String workspace, String storename) { + try { + URL deleteStore = new URL(restURL + "/rest/workspaces/" + workspace + "/coveragestores/" + storename); + boolean deleted = HTTPUtils.delete(deleteStore.toExternalForm(), gsuser, gspass); + if (!deleted) { + LOGGER.warn("Could not delete CoverageStore " + workspace + ":" + storename); + } else { + LOGGER.info("CoverageStore successfully deleted " + workspace + ":" + storename); + } + + return deleted; + } catch (MalformedURLException ex) { + LOGGER.error(ex); + return false; + } + } + + /** + * Remove a given Workspace. + * + * @param workspace The name of the workspace + * @return true if the WorkSpace was successfully removed. + */ + public boolean removeWorkspace(String workspace) { + workspace = sanitize(workspace); + try { + URL deleteUrl = new URL(restURL + "/rest/workspaces/" + workspace ); + boolean deleted = HTTPUtils.delete(deleteUrl.toExternalForm(), gsuser, gspass); + if (!deleted) { + LOGGER.warn("Could not delete Workspace " + workspace ); + } else { + LOGGER.info("Workspace successfully deleted " + workspace ); + } + + return deleted; + } catch (MalformedURLException ex) { + LOGGER.error(ex); + return false; + } + } + + public boolean removeLayerGroup(String name) { + try { + URL deleteUrl = new URL(restURL + "/rest/layergroups/" + name); + boolean deleted = HTTPUtils.delete(deleteUrl.toExternalForm(), gsuser, gspass); + if (!deleted) { + LOGGER.warn("Could not delete layergroup " + name); + } else { + LOGGER.info("Layergroup successfully deleted: " + name); + } + + return deleted; + } catch (MalformedURLException ex) { + LOGGER.error(ex); + return false; + } + } + + //========================================================================== + //=== + //========================================================================== + + /** + * Allows to configure some layer attributes such as WmsPath and DefaultStyle + * + */ + protected boolean configureLayer(final GSLayerEncoder layer, final String layerName) { + + if (layer.isEmpty()) { + return true; + } + + final String url = restURL + "/rest/layers/" + layerName; + + String layerXml = layer.encodeXml(); + String sendResult = HTTPUtils.putXml(url, layerXml, gsuser, gspass); + if (sendResult != null) { + if (LOGGER.isInfoEnabled()) { + LOGGER.info("Layer successfully configured: " + layerName); + } + } else { + LOGGER.warn("Error configuring layer " + layerName + " ("+sendResult+")"); + } + + return sendResult != null; + } + + /** + * Allows to configure some coverage's attributes + * + */ + protected boolean configureCoverage(final GSCoverageEncoder ce, String wsname, String csname, String cname) { + + final String url = restURL + "/rest/workspaces/"+wsname+"/coveragestores/"+csname+"/coverages/"+cname+".xml"; + + String xmlBody = ce.encodeXml(); + String sendResult = HTTPUtils.putXml(url, xmlBody, gsuser, gspass); + if (sendResult != null) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Coverage successfully configured "+wsname+":"+csname+":"+cname); + } + } else { + LOGGER.warn("Error configuring coverage " + wsname+":"+csname+":"+cname +" ("+sendResult+")"); + } + + return sendResult != null; + } + + /** + * + */ + protected String sanitize(String s) { + if(s.indexOf(".")!=-1) + return s+".DUMMY"; + return s; + } + + protected String encode(String s) { + return URLEncoder.encode(s); + } + +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTReader.java b/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTReader.java new file mode 100644 index 0000000..bf83b7e --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/GeoServerRESTReader.java @@ -0,0 +1,515 @@ +/* + * 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; + +import it.geosolutions.geoserver.rest.decoder.RESTCoverage; +import it.geosolutions.geoserver.rest.decoder.RESTCoverageList; +import it.geosolutions.geoserver.rest.decoder.RESTCoverageStore; +import it.geosolutions.geoserver.rest.decoder.RESTCoverageStoreList; +import it.geosolutions.geoserver.rest.decoder.RESTDataStoreList; +import it.geosolutions.geoserver.rest.decoder.RESTDataStore; +import it.geosolutions.geoserver.rest.decoder.RESTFeatureType; +import it.geosolutions.geoserver.rest.decoder.RESTLayer; +import it.geosolutions.geoserver.rest.decoder.RESTLayerGroup; +import it.geosolutions.geoserver.rest.decoder.RESTLayerGroupList; +import it.geosolutions.geoserver.rest.decoder.RESTLayerList; +import it.geosolutions.geoserver.rest.decoder.RESTNamespaceList; +import it.geosolutions.geoserver.rest.decoder.RESTResource; +import it.geosolutions.geoserver.rest.decoder.RESTStyleList; +import it.geosolutions.geoserver.rest.decoder.RESTWorkspaceList; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import org.apache.log4j.Logger; + + +/** + * Connect to a GeoServer instance to read its data. + *
Info are returned as Strings or, for complex data, as XML elements + * wrapped in proper parsers (e.g.: {@link RESTLayer}, {@link RESTCoverageStore}, ...). + * + * @author ETj (etj at geo-solutions.it) + */ +public class GeoServerRESTReader { + + private final static Logger LOGGER = Logger.getLogger(GeoServerRESTReader.class); + private final String baseurl; + private String username; + private String password; + + /** + * Creates a GeoServerRESTReader for a given GeoServer instance and + * no auth credentials. + *

Note that GeoServer 2.0 REST interface requires username/password credentials by + * default, if not otherwise configured. . + * + * @param restUrl the base GeoServer URL(e.g.: http://localhost:8080/geoserver) + */ + public GeoServerRESTReader(URL restUrl) { + String extForm = restUrl.toExternalForm(); + this.baseurl = extForm.endsWith("/") ? + extForm.substring(0, extForm.length()-1) : + extForm; + } + + /** + * Creates a GeoServerRESTReader for a given GeoServer instance and + * no auth credentials. + *

Note that GeoServer 2.0 REST interface requires username/password credentials by + * default, if not otherwise configured. . + * + * @param restUrl the base GeoServer URL (e.g.: http://localhost:8080/geoserver) + */ + public GeoServerRESTReader(String restUrl) + throws MalformedURLException { + new URL(restUrl); // check URL correctness + this.baseurl = restUrl.endsWith("/") ? + restUrl.substring(0, restUrl.length()-1) : + restUrl; + } + + /** + * Creates a GeoServerRESTReader for a given GeoServer instance + * with the given auth credentials. + * + * @param restUrl the base GeoServer URL (e.g.: http://localhost:8080/geoserver) + * @param username username auth credential + * @param password password auth credential + */ + public GeoServerRESTReader(String restUrl, String username, String password) throws MalformedURLException { + this(restUrl); + this.username = username; + this.password = password; + } + + /** + * Creates a GeoServerRESTReader for a given GeoServer instance + * with the given auth credentials. + * + * @param restUrl the base GeoServer URL (e.g.: http://localhost:8080/geoserver) + * @param username username auth credential + * @param password password auth credential + */ + public GeoServerRESTReader(URL restUrl, String username, String password) { + this(restUrl); + this.username = username; + this.password = password; + } + + private String load(String url) { + LOGGER.info("Loading from REST path " + url); + try { + String response = HTTPUtils.get(baseurl + url, username, password); + return response; + } catch (MalformedURLException ex) { + LOGGER.warn("Bad URL", ex); + } + + return null; + } + + private String loadFullURL(String url) { + LOGGER.info("Loading from REST path " + url); + try { + String response = HTTPUtils.get(url, username, password); + return response; + } catch (MalformedURLException ex) { + LOGGER.warn("Bad URL", ex); + } + return null; + } + + /** + * Check if a GeoServer instance is running at the given URL. + *
+ * Return true if the configured GeoServer is up and replies to REST requests. + *
+ * Send a HTTP GET request to the configured URL.
+ * Return true if a HTTP 200 code (OK) is read from the HTTP response; + * any other response code, or connection error, will return a + * false boolean. + * + * @return true if a GeoServer instance was found at the configured URL. + */ + public boolean existGeoserver() { + return HTTPUtils.httpPing(baseurl + "/rest/", username, password); + } + + //========================================================================== + //=== STYLES + //========================================================================== + + /** + * Check if a Style exists in the configured GeoServer instance. + * @param styleName the name of the style to check for. + * @return true on HTTP 200, false on HTTP 404 + * @throws RuntimeException if any other HTTP code than 200 or 404 was retrieved. + */ + public boolean existsStyle(String styleName) throws RuntimeException { + String url = baseurl + "/rest/styles/" + styleName + ".xml"; + return HTTPUtils.exists(url, username, password); + } + + /** + * Get summary info about all Styles. + * + * @return summary info about Styles as a {@link RESTStyleList} + */ + public RESTStyleList getStyles() { + String url = "/rest/styles.xml"; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("### Retrieving Styles list from " + url); + } + return RESTStyleList.build(load(url)); + } + + /** + * Get the SLD body of a Style. + */ + public String getSLD(String styleName) { + String url = "/rest/styles/"+styleName+".sld"; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("### Retrieving SLD body from " + url); + } + return load(url); + } + + //========================================================================== + //=== DATASTORES + //========================================================================== + + /** + * Get summary info about all DataStores in a WorkSpace. + * + * @param workspace The name of the workspace + * + * @return summary info about Datastores as a {@link RESTDataStoreList} + */ + public RESTDataStoreList getDatastores(String workspace) { + String url = "/rest/workspaces/" + workspace + "/datastores.xml"; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("### Retrieving DS list from " + url); + } + return RESTDataStoreList.build(load(url)); + } + + /** + * Get detailed info about a given Datastore in a given Workspace. + * + * @param workspace The name of the workspace + * @param dsName The name of the Datastore + * @return DataStore details as a {@link RESTDataStore} + */ + public RESTDataStore getDatastore(String workspace, String dsName) { + String url = "/rest/workspaces/" + workspace + "/datastores/" + dsName + ".xml"; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("### Retrieving DS from " + url); + } + String response = load(url); +// System.out.println("DATASTORE " + workspace+":"+dsName+"\n"+response); + return RESTDataStore.build(response); + } + + /** + * Get detailed info about a FeatureType's Datastore. + * + * @param featureType the RESTFeatureType + * @return DataStore details as a {@link RESTDataStore} + */ + public RESTDataStore getDatastore(RESTFeatureType featureType) { + + String url = featureType.getStoreUrl(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("### Retrieving DS from fullurl " + url); + } + String response = loadFullURL(url); + return RESTDataStore.build(response); + } + + //========================================================================== + //=== FEATURETYPES + //========================================================================== + + /** + * Get detailed info about a FeatureType given the Layer where it's published with. + * + * @param layer A layer publishing the FeatureType + * @return FeatureType details as a {@link RESTCoverage} + */ + + public RESTFeatureType getFeatureType(RESTLayer layer) { + if(layer.getType() != RESTLayer.TYPE.VECTOR) + throw new RuntimeException("Bad layer type for layer " + layer.getName()); + + String response = loadFullURL(layer.getResourceUrl()); + return RESTFeatureType.build(response); + } + + //========================================================================== + //=== COVERAGESTORES + //========================================================================== + + /** + * Get summary info about all CoverageStores in a WorkSpace. + * + * @param workspace The name of the workspace + * + * @return summary info about CoverageStores as a {@link RESTDataStoreList} + */ + public RESTCoverageStoreList getCoverageStores(String workspace) { + String url = "/rest/workspaces/" + workspace + "/coveragestores.xml"; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("### Retrieving CS list from " + url); + } + return RESTCoverageStoreList.build(load(url)); + } + + /** + * Get detailed info about a given CoverageStore in a given Workspace. + * + * @param workspace The name of the workspace + * @param csName The name of the CoverageStore + * @return CoverageStore details as a {@link RESTCoverageStore} + */ + public RESTCoverageStore getCoverageStore(String workspace, String csName) { + String url = "/rest/workspaces/" + workspace + "/coveragestores/" + csName + ".xml"; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("### Retrieving CS from " + url); + } + return RESTCoverageStore.build(load(url)); + } + + /** + * Get detailed info about a Coverage's Datastore. + * + * @param coverage the RESTFeatureType + * @return CoverageStore details as a {@link RESTCoverageStore} + */ + public RESTCoverageStore getCoverageStore(RESTCoverage coverage) { + + String url = coverage.getStoreUrl(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("### Retrieving CS from fullurl " + url); + } + String response = loadFullURL(url); + return RESTCoverageStore.build(response); + } + + //========================================================================== + //=== COVERAGES + //========================================================================== + + /** + * Get list of coverages (usually only one). + * + * @param workspace The name of the workspace + * @param csName The name of the CoverageStore + * @return Coverages list as a {@link RESTCoverageList} + */ + public RESTCoverageList getCoverages(String workspace, String csName) { + // restURL + "/rest/workspaces/" + workspace + "/coveragestores/" + coverageStore + "/coverages.xml"; + String url = "/rest/workspaces/" + workspace + "/coveragestores/" + csName + "/coverages.xml"; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("### Retrieving Covs from " + url); + } + return RESTCoverageList.build(load(url)); + } + + /** + * Get detailed info about a given Coverage. + * + * @param workspace The name of the workspace + * @param store The name of the CoverageStore + * @param name The name of the Coverage + * @return Coverage details as a {@link RESTCoverage} + */ + public RESTCoverage getCoverage(String workspace, String store, String name) { + String url = "/rest/workspaces/" + workspace + "/coveragestores/" + store + "/coverages/"+name+".xml"; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("### Retrieving Coverage from " + url); + } + return RESTCoverage.build(load(url)); + } + + /** + * Get detailed info about a Coverage given the Layer where it's published with. + * + * @param layer A layer publishing the CoverageStore + * @return Coverage details as a {@link RESTCoverage} + */ + public RESTCoverage getCoverage(RESTLayer layer) { + if(layer.getType() != RESTLayer.TYPE.RASTER) + throw new RuntimeException("Bad layer type for layer " + layer.getName()); + + String response = loadFullURL(layer.getResourceUrl()); + return RESTCoverage.build(response); + } + + //========================================================================== + //========================================================================== + + /** + * Get detailed info about a Resource given the Layer where it's published with. + * The Resource can then be converted to RESTCoverage or RESTFeatureType + * + * @return Resource details as a {@link RESTResource} + */ + public RESTResource getResource(RESTLayer layer) { + String response = loadFullURL(layer.getResourceUrl()); + return RESTResource.build(response); + } + + //========================================================================== + //=== LAYERGROUPS + //========================================================================== + + /** + * Get summary info about all LayerGroups. + * + * @return summary info about LayerGroups as a {@link RESTLayerGroupList} + */ + public RESTLayerGroupList getLayerGroups() { + String url = "/rest/layergroups.xml"; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("### Retrieving layergroups from " + url); + } + return RESTLayerGroupList.build(load(url)); + } + + /** + * Get detailed info about a given LayerGroup. + * + * @param name The name of the LayerGroup + * @return LayerGroup details as a {@link RESTLayerGroup} + */ + public RESTLayerGroup getLayerGroup(String name) { + String url = "/rest/layergroups/" + name + ".xml"; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("### Retrieving layergroup from " + url); + } + return RESTLayerGroup.build(load(url)); + } + + //========================================================================== + //=== LAYERS + //========================================================================== + + /** + * Get summary info about all Layers. + * + * @return summary info about Layers as a {@link RESTLayerList} + */ + public RESTLayerList getLayers() { + String url = "/rest/layers.xml"; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("### Retrieving layers from " + url); + } + return RESTLayerList.build(load(url)); + } + + /** + * Get detailed info about a given Layer. + * + * @param name The name of the Layer + * @return Layer details as a {@link RESTLayer} + */ + public RESTLayer getLayer(String name) { + String url = "/rest/layers/" + name + ".xml"; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("### Retrieving layer from " + url); + } + return RESTLayer.build(load(url)); + } + + //========================================================================== + //=== NAMESPACES + //========================================================================== + + /** + * Get summary info about all Namespaces. + * + * @return summary info about Namespaces as a {@link RESTNamespaceList} + */ + public RESTNamespaceList getNamespaces() { + String url = "/rest/namespaces.xml"; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("### Retrieving namespaces from " + url); + } + return RESTNamespaceList.build(load(url)); + } + + /** + * Get the names of all the Namespaces. + *
+ * This is a shortcut call: These info could be retrieved using {@link #getNamespaces getNamespaces} + * @return the list of the names of all Namespaces. + */ + public List getNamespaceNames() { + RESTNamespaceList list = getNamespaces(); + List names = new ArrayList(list.size()); + for (RESTNamespaceList.RESTShortNamespace item : list) { + names.add(item.getName()); + } + return names; + } + + //========================================================================== + //=== WORKSPACES + //========================================================================== + + /** + * Get summary info about all Workspaces. + * + * @return summary info about Workspaces as a {@link RESTWorkspaceList} + */ + public RESTWorkspaceList getWorkspaces() { + String url = "/rest/workspaces.xml"; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("### Retrieving workspaces from " + url); + } + + return RESTWorkspaceList.build(load(url)); + } + + /** + * Get the names of all the Workspaces. + *
+ * This is a shortcut call: These info could be retrieved using {@link #getWorkspaces getWorkspaces} + * @return the list of the names of all Workspaces. + */ + public List getWorkspaceNames() { + RESTWorkspaceList list = getWorkspaces(); + List names = new ArrayList(list.size()); + for (RESTWorkspaceList.RESTShortWorkspace item : list) { + names.add(item.getName()); + } + return names; + } + +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/HTTPUtils.java b/src/main/java/it/geosolutions/geoserver/rest/HTTPUtils.java new file mode 100644 index 0000000..8a612ac --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/HTTPUtils.java @@ -0,0 +1,430 @@ +/* + * 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; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.ConnectException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import org.apache.commons.httpclient.Credentials; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.UsernamePasswordCredentials; +import org.apache.commons.httpclient.auth.AuthScope; +import org.apache.commons.httpclient.methods.DeleteMethod; +import org.apache.commons.httpclient.methods.EntityEnclosingMethod; +import org.apache.commons.httpclient.methods.FileRequestEntity; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.methods.PutMethod; +import org.apache.commons.httpclient.methods.RequestEntity; +import org.apache.commons.httpclient.methods.StringRequestEntity; +import org.apache.commons.io.IOUtils; +import org.apache.log4j.Logger; + +/** + * Low level HTTP utilities. + */ +class HTTPUtils { + private static final Logger LOGGER = Logger.getLogger(HTTPUtils.class); + + /** + * Performs an HTTP GET on the given URL. + * + * @param url The URL where to connect to. + * @return The HTTP response as a String if the HTTP response code was 200 (OK). + * @throws MalformedURLException + */ + public static String get(String url) throws MalformedURLException { + return get(url, null, null); + } + + /** + * Performs an HTTP GET on the given URL. + *
Basic auth is used if both username and pw are not null. + * + * @param url The URL where to connect to. + * @param username Basic auth credential. No basic auth if null. + * @param pw Basic auth credential. No basic auth if null. + * @return The HTTP response as a String if the HTTP response code was 200 (OK). + * @throws MalformedURLException + */ + public static String get(String url, String username, String pw) throws MalformedURLException { + + GetMethod httpMethod = null; + try { + HttpClient client = new HttpClient(); + setAuth(client, url, username, pw); + httpMethod = new GetMethod(url); + client.getHttpConnectionManager().getParams().setConnectionTimeout(5000); + int status = client.executeMethod(httpMethod); + if(status == HttpStatus.SC_OK) { + InputStream is = httpMethod.getResponseBodyAsStream(); + String response = IOUtils.toString(is); + if(response.trim().length()==0) { // sometime gs rest fails + LOGGER.warn("ResponseBody is empty"); + return null; + } else { + return response; + } + } else { + LOGGER.info("("+status+") " + HttpStatus.getStatusText(status) + " -- " + url ); + } + } catch (ConnectException e) { + LOGGER.info("Couldn't connect to ["+url+"]"); + } catch (IOException e) { + LOGGER.info("Error talking to ["+url+"]", e); + } finally { + if(httpMethod != null) + httpMethod.releaseConnection(); + } + + return null; + } + + /** + * PUTs a File to the given URL. + *
Basic auth is used if both username and pw are not null. + * + * @param url The URL where to connect to. + * @param file The File to be sent. + * @param contentType The content-type to advert in the PUT. + * @param username Basic auth credential. No basic auth if null. + * @param pw Basic auth credential. No basic auth if null. + * @return The HTTP response as a String if the HTTP response code was 200 (OK). + * @throws MalformedURLException + * @return the HTTP response or null on errors. + */ + public static String put(String url, File file, String contentType, String username, String pw) { + return put(url, new FileRequestEntity(file, contentType), username, pw); + } + + /** + * PUTs a String to the given URL. + *
Basic auth is used if both username and pw are not null. + * + * @param url The URL where to connect to. + * @param content The content to be sent as a String. + * @param contentType The content-type to advert in the PUT. + * @param username Basic auth credential. No basic auth if null. + * @param pw Basic auth credential. No basic auth if null. + * @return The HTTP response as a String if the HTTP response code was 200 (OK). + * @throws MalformedURLException + * @return the HTTP response or null on errors. + */ + public static String put(String url, String content, String contentType, String username, String pw) { + try { + return put(url, new StringRequestEntity(content, contentType, null), username, pw); + } catch (UnsupportedEncodingException ex) { + LOGGER.error("Cannot PUT " + url, ex); + return null; + } + } + + /** + * PUTs a String representing an XML document to the given URL. + *
Basic auth is used if both username and pw are not null. + * + * @param url The URL where to connect to. + * @param content The XML content to be sent as a String. + * @param username Basic auth credential. No basic auth if null. + * @param pw Basic auth credential. No basic auth if null. + * @return The HTTP response as a String if the HTTP response code was 200 (OK). + * @throws MalformedURLException + * @return the HTTP response or null on errors. + */ + public static String putXml(String url, String content, String username, String pw) { + return put(url, content, "text/xml", username, pw); + } + + /** + * Performs a PUT to the given URL. + *
Basic auth is used if both username and pw are not null. + * + * @param url The URL where to connect to. + * @param requestEntity The request to be sent. + * @param username Basic auth credential. No basic auth if null. + * @param pw Basic auth credential. No basic auth if null. + * @return The HTTP response as a String if the HTTP response code was 200 (OK). + * @throws MalformedURLException + * @return the HTTP response or null on errors. + */ + public static String put(String url, RequestEntity requestEntity, String username, String pw) { + return send(new PutMethod(url), url, requestEntity, username, pw); + } + + /** + * POSTs a File to the given URL. + *
Basic auth is used if both username and pw are not null. + * + * @param url The URL where to connect to. + * @param file The File to be sent. + * @param contentType The content-type to advert in the POST. + * @param username Basic auth credential. No basic auth if null. + * @param pw Basic auth credential. No basic auth if null. + * @return The HTTP response as a String if the HTTP response code was 200 (OK). + * @throws MalformedURLException + * @return the HTTP response or null on errors. + */ + public static String post(String url, File file, String contentType, String username, String pw) { + return post(url, new FileRequestEntity(file, contentType), username, pw); + } + + /** + * POSTs a String to the given URL. + *
Basic auth is used if both username and pw are not null. + * + * @param url The URL where to connect to. + * @param content The content to be sent as a String. + * @param contentType The content-type to advert in the POST. + * @param username Basic auth credential. No basic auth if null. + * @param pw Basic auth credential. No basic auth if null. + * @return The HTTP response as a String if the HTTP response code was 200 (OK). + * @throws MalformedURLException + * @return the HTTP response or null on errors. + */ + public static String post(String url, String content, String contentType, String username, String pw) { + try { + return post(url, new StringRequestEntity(content, contentType, null), username, pw); + } catch (UnsupportedEncodingException ex) { + LOGGER.error("Cannot POST " + url, ex); + return null; + } + } + + /** + * POSTs a String representing an XML document to the given URL. + *
Basic auth is used if both username and pw are not null. + * + * @param url The URL where to connect to. + * @param content The XML content to be sent as a String. + * @param username Basic auth credential. No basic auth if null. + * @param pw Basic auth credential. No basic auth if null. + * @return The HTTP response as a String if the HTTP response code was 200 (OK). + * @throws MalformedURLException + * @return the HTTP response or null on errors. + */ + public static String postXml(String url, String content, String username, String pw) { + return post(url, content, "text/xml", username, pw); + } + + /** + * Performs a POST to the given URL. + *
Basic auth is used if both username and pw are not null. + * + * @param url The URL where to connect to. + * @param requestEntity The request to be sent. + * @param username Basic auth credential. No basic auth if null. + * @param pw Basic auth credential. No basic auth if null. + * @return The HTTP response as a String if the HTTP response code was 200 (OK). + * @throws MalformedURLException + * @return the HTTP response or null on errors. + */ + public static String post(String url, RequestEntity requestEntity, String username, String pw) { + return send(new PostMethod(url), url, requestEntity, username, pw); + } + + /** + * Send an HTTP request (PUT or POST) to a server. + *
Basic auth is used if both username and pw are not null. + *

+ * Only

are accepted as successful codes; in these cases the response string will be returned. + * + * @return the HTTP response or null on errors. + */ + private static String send(final EntityEnclosingMethod httpMethod, String url, RequestEntity requestEntity, String username, String pw) { + + try { + HttpClient client = new HttpClient(); + setAuth(client, url, username, pw); +// httpMethod = new PutMethod(url); + client.getHttpConnectionManager().getParams().setConnectionTimeout(5000); + if(requestEntity != null) + httpMethod.setRequestEntity(requestEntity); + int status = client.executeMethod(httpMethod); + + switch(status) { + case HttpURLConnection.HTTP_OK: + case HttpURLConnection.HTTP_CREATED: + case HttpURLConnection.HTTP_ACCEPTED: + String response = IOUtils.toString(httpMethod.getResponseBodyAsStream()); +// LOGGER.info("================= POST " + url); + LOGGER.info("HTTP "+ httpMethod.getStatusText()+": " + response); + return response; + default: + LOGGER.warn("Bad response: code["+status+"]" + + " msg["+httpMethod.getStatusText()+"]" + + " url["+url+"]" + + " method["+httpMethod.getClass().getSimpleName()+"]: " + + IOUtils.toString(httpMethod.getResponseBodyAsStream()) + ); + return null; + } + } catch (ConnectException e) { + LOGGER.info("Couldn't connect to ["+url+"]"); + return null; + } catch (IOException e) { + LOGGER.error("Error talking to " + url + " : " + e.getLocalizedMessage()); + return null; + } finally { + if(httpMethod != null) + httpMethod.releaseConnection(); + } + } + + public static boolean delete(String url, final String user, final String pw) { + + DeleteMethod httpMethod = null; + + try { + HttpClient client = new HttpClient(); + setAuth(client, url, user, pw); + httpMethod = new DeleteMethod(url); + client.getHttpConnectionManager().getParams().setConnectionTimeout(5000); + int status = client.executeMethod(httpMethod); + String response = ""; + if(status == HttpStatus.SC_OK) { + InputStream is = httpMethod.getResponseBodyAsStream(); + response = IOUtils.toString(is); + if(response.trim().equals("")) { // sometimes gs rest fails + if(LOGGER.isDebugEnabled()) + LOGGER.debug("ResponseBody is empty (this may be not an error since we just performed a DELETE call)"); + return true; + } + if(LOGGER.isDebugEnabled()) + LOGGER.debug("("+status+") " + httpMethod.getStatusText() + " -- " + url ); + return true; + } else { + LOGGER.info("("+status+") " + httpMethod.getStatusText() + " -- " + url ); + LOGGER.info("Response: '"+response+"'" ); + } + } catch (ConnectException e) { + LOGGER.info("Couldn't connect to ["+url+"]"); + } catch (IOException e) { + LOGGER.info("Error talking to ["+url+"]", e); + } finally { + if(httpMethod != null) + httpMethod.releaseConnection(); + } + + return false; + } + + /** + * @return true if the server response was an HTTP_OK + */ + public static boolean httpPing(String url) { + return httpPing(url, null, null); + } + + public static boolean httpPing(String url, String username, String pw) { + + GetMethod httpMethod = null; + + try { + HttpClient client = new HttpClient(); + setAuth(client, url, username, pw); + httpMethod = new GetMethod(url); + client.getHttpConnectionManager().getParams().setConnectionTimeout(2000); + int status = client.executeMethod(httpMethod); + if(status != HttpStatus.SC_OK) { + LOGGER.warn("PING failed at '"+url+"': ("+status+") " + httpMethod.getStatusText()); + return false; + } else { + return true; + } + + } catch (ConnectException e) { + return false; + } catch (IOException e) { + e.printStackTrace(); + return false; + } finally { + if(httpMethod != null) + httpMethod.releaseConnection(); + } + } + + /** + * Used to query for REST resources. + * + * @param url The URL of the REST resource to query about. + * @param username + * @param pw + * @return true on 200, false on 404. + * @throws RuntimeException on unhandled status or exceptions. + */ + public static boolean exists(String url, String username, String pw) { + + GetMethod httpMethod = null; + + try { + HttpClient client = new HttpClient(); + setAuth(client, url, username, pw); + httpMethod = new GetMethod(url); + client.getHttpConnectionManager().getParams().setConnectionTimeout(2000); + int status = client.executeMethod(httpMethod); + switch(status) { + case HttpStatus.SC_OK: + return true; + case HttpStatus.SC_NOT_FOUND: + return false; + default: + throw new RuntimeException("Unhandled response status at '"+url+"': ("+status+") " + httpMethod.getStatusText()); + } + } catch (ConnectException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + if(httpMethod != null) + httpMethod.releaseConnection(); + } + } + + + private static void setAuth(HttpClient client, String url, String username, String pw) throws MalformedURLException { + URL u = new URL(url); + if(username != null && pw != null) { + Credentials defaultcreds = new UsernamePasswordCredentials(username, pw); + client.getState().setCredentials(new AuthScope(u.getHost(), u.getPort()), defaultcreds); + client.getParams().setAuthenticationPreemptive(true); // GS2 by default always requires authentication + } else { + if(LOGGER.isDebugEnabled()) { + LOGGER.debug("Not setting credentials to access to " + url); + } + } + } + +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTAbstractList.java b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTAbstractList.java new file mode 100644 index 0000000..3ea2069 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTAbstractList.java @@ -0,0 +1,121 @@ +/* + * 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.decoder; + +import it.geosolutions.geoserver.rest.decoder.utils.JDOMListIterator; + +import it.geosolutions.geoserver.rest.decoder.utils.NameLinkElem; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.jdom.Element; + +/** + * Parses list of summary data. + * + *

This is the XML REST representation: + *

+  {@code
+
+      
+        elem1
+        
+      
+      
+        elem2
+        
+      
+
+}
+ * + * @author ETj (etj at geo-solutions.it) + */ +public class RESTAbstractList implements Iterable { + + protected final List elementList; + + protected RESTAbstractList(Element list) { + List tempList = new ArrayList(); + String baseName = null; + + for (Element listItem : (List) list.getChildren()) { + if(baseName == null) + baseName = listItem.getName(); + else + if(! baseName.equals(listItem.getName())) { + throw new RuntimeException("List elements mismatching (" + baseName+","+listItem.getName()+")"); + } + + tempList.add(listItem); + } + + elementList = Collections.unmodifiableList(tempList); + } + + public int size() { + return elementList.size(); + } + + public boolean isEmpty() { + return elementList.isEmpty(); + } + + public ELEM get(int index) { + return createElement(elementList.get(index)); + } + + public Iterator iterator() { + return new RESTAbstractListIterator(elementList); + } + + public List getNames() { + List names = new ArrayList(elementList.size()); + for (ELEM elem: this) { + names.add(elem.getName()); + } + return names; + } + + + private class RESTAbstractListIterator extends JDOMListIterator { + + public RESTAbstractListIterator(List orig) { + super(orig); + } + + @Override + public ELEM transform(Element listItem) { + return createElement(listItem); + } + } + + protected ELEM createElement(Element el) { + return (ELEM)new NameLinkElem(el); + } + +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTCoverage.java b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTCoverage.java new file mode 100644 index 0000000..f5a343e --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTCoverage.java @@ -0,0 +1,240 @@ +/* + * 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.decoder; + +import it.geosolutions.geoserver.rest.decoder.utils.JDOMBuilder; +import org.jdom.Element; + +/** + * Parse Coverages returned as XML REST objects. + * + *

This is the XML REST representation: + *

+ *{@code
+
+  sfdem
+  sfdem
+  
+    sf
+    
+  
+  sfdem is a Tagged Image File Format with Geographic information
+  Generated from sfdem
+  
+    WCS
+    sfdem
+    sfdem
+  
+  PROJCS["NAD27 / UTM zone 13N",
+  GEOGCS["NAD27",
+    DATUM["North American Datum 1927",
+      SPHEROID["Clarke 1866", 6378206.4, 294.9786982138982, AUTHORITY["EPSG","7008"]],
+      TOWGS84[-4.2, 135.4, 181.9, 0.0, 0.0, 0.0, 0.0],
+      AUTHORITY["EPSG","6267"]],
+    PRIMEM["Greenwich", 0.0, AUTHORITY["EPSG","8901"]],
+    UNIT["degree", 0.017453292519943295],
+    AXIS["Geodetic longitude", EAST],
+    AXIS["Geodetic latitude", NORTH],
+    AUTHORITY["EPSG","4267"]],
+  PROJECTION["Transverse_Mercator"],
+  PARAMETER["central_meridian", -105.0],
+  PARAMETER["latitude_of_origin", 0.0],
+  PARAMETER["scale_factor", 0.9996],
+  PARAMETER["false_easting", 500000.0],
+  PARAMETER["false_northing", 0.0],
+  UNIT["m", 1.0],
+  AXIS["Easting", EAST],
+  AXIS["Northing", NORTH],
+  AUTHORITY["EPSG","26713"]]
+  EPSG:26713
+  
+    589980.0
+    609000.0
+    4913700.0
+    4928010.0
+    EPSG:26713
+  
+  
+    -103.87108701853181
+    -103.62940739432703
+    44.370187074132616
+    44.5016011535299
+    EPSG:4326
+  
+  true
+  
+    sfdem_sfdem
+  
+  
+    sfdem
+    
+  
+  GeoTIFF
+  
+    
+      0 0
+      634 477
+    
+    
+      30.0
+      -30.0
+      0.0
+      0.0
+      589995.0
+      4927995.0
+    
+    EPSG:26713
+  
+  
+    ARCGRID
+    IMAGEMOSAIC
+    GTOPO30
+    GEOTIFF
+    GIF
+    PNG
+    JPEG
+    TIFF
+  
+  
+    nearest neighbor
+    bilinear
+    bicubic
+  
+  nearest neighbor
+  
+    
+      GRAY_INDEX
+      GridSampleDimension[-9.999999933815813E36,-9.999999933815813E36]
+      
+        -9.999999933815813E36
+        -9.999999933815813E36
+      
+    
+  
+  
+    EPSG:26713
+  
+  
+    EPSG:26713
+  
+
+ * }
+ * + * @author etj + */ +public class RESTCoverage extends RESTResource { + + + public static RESTCoverage build(String response) { + Element elem = JDOMBuilder.buildElement(response); + return elem == null? null : new RESTCoverage(elem); + } + + public RESTCoverage(Element resource) { + super(resource); + } + + public RESTCoverage(RESTResource resource) { + super(resource.rootElem); + } + +// public String getName() { +// return rootElem.getChildText("name"); +// } + +// public String getNativeName() { +// return rootElem.getChildText("nativeName"); +// } + + public String getNativeFormat() { + return rootElem.getChildText("nativeFormat"); + } + +// public String getNameSpace() { +// return rootElem.getChild("namespace").getChildText("name"); +// } +// +// public String getTitle() { +// return rootElem.getChildText("title"); +// } + + public String getNativeCRS() { + return rootElem.getChildText("nativeCRS"); + } + + public String getSRS() { + return rootElem.getChildText("srs"); + } + + +// public String getStoreName() { +// return rootElem.getChild("store").getChildText("name"); +// } +// +// public String getStoreType() { +// return rootElem.getChild("store").getAttributeValue("class"); +// } + + /** + * Get the URL to retrieve the featuretype. + *
 {@code
+     * 
+     *      sfdem
+     *      
+     * 
+     * }
+     */
+//    public String getStoreUrl() {
+//		Element store = rootElem.getChild("store");
+//        Element atom = store.getChild("link", Namespace.getNamespace("atom", "http://www.w3.org/2005/Atom"));
+//        return atom.getAttributeValue("href");
+//    }
+
+//	public String getBBCRS() {
+//		Element elBBox = rootElem.getChild("latLonBoundingBox");
+//		return elBBox.getChildText("crs");
+//	}
+//
+//	protected double getLatLonEdge(String edge) {
+//		Element elBBox = rootElem.getChild("latLonBoundingBox");
+//		return Double.parseDouble(elBBox.getChildText(edge));
+//	}
+//
+//	public double getMinX() {
+//		return getLatLonEdge("minx");
+//	}
+//	public double getMaxX() {
+//		return getLatLonEdge("maxx");
+//	}
+//	public double getMinY() {
+//		return getLatLonEdge("miny");
+//	}
+//	public double getMaxY() {
+//		return getLatLonEdge("maxy");
+//	}
+}
diff --git a/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTCoverageList.java b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTCoverageList.java
new file mode 100644
index 0000000..9dcb575
--- /dev/null
+++ b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTCoverageList.java
@@ -0,0 +1,62 @@
+/*
+ *  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.decoder;
+
+import it.geosolutions.geoserver.rest.decoder.utils.NameLinkElem;
+import it.geosolutions.geoserver.rest.decoder.utils.JDOMBuilder;
+import org.jdom.Element;
+
+/**
+ * Parses list of summary data about Coverages.
+ *
+ * 

This is the XML REST representation: + *

{@code 
+  
+    5-25-1-120-11-DOF
+    
+  
+
+ *
+}
+ * + * @author ETj (etj at geo-solutions.it) + */ +public class RESTCoverageList extends RESTAbstractList { + + public static RESTCoverageList build(String response) { + Element elem = JDOMBuilder.buildElement(response); + return elem == null? null : new RESTCoverageList(elem); + } + + protected RESTCoverageList(Element list) { + super(list); + } + +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTCoverageStore.java b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTCoverageStore.java new file mode 100644 index 0000000..69b2863 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTCoverageStore.java @@ -0,0 +1,98 @@ +/* + * 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.decoder; + +import it.geosolutions.geoserver.rest.decoder.utils.JDOMBuilder; +import org.jdom.Element; + +/** + * Parse CoverageStores returned as XML REST objects. + *

+ * This is the XML document returned by GeoServer when requesting a CoverageStore: + *

+ * {@code 
+ * 
+ *      testRESTStoreGeotiff
+ *      GeoTIFF
+ *      true
+ *      
+ *          it.geosolutions
+ *          http://localhost:8080/geoserver/rest/workspaces/it.geosolutions.xml
+ *      
+ *      file:/home/geosolutions/prj/git/gman/target/test-classes/testdata/resttestdem.tif
+ *      
+ *          
+ *      
+ * 
+ * }
+ * 
+ * + * Note: the whole XML fragment is stored in memory. At the moment, there are + * methods to retrieve only the more useful data. + * + * @author etj + */ +public class RESTCoverageStore { + private final Element cs; + + + public RESTCoverageStore(Element cs) { + this.cs = cs; + } + + public static RESTCoverageStore build(String response) { + if(response == null) + return null; + + Element pb = JDOMBuilder.buildElement(response); + if(pb != null) + return new RESTCoverageStore(pb); + else + return null; + } + + public String getName() { + return cs.getChildText("name"); + } + + public String getWorkspaceName() { + return cs.getChild("workspace").getChildText("name"); + } + + public String toString() { + StringBuilder sb = new StringBuilder(getClass().getSimpleName()) + .append('['); + if(cs == null) + sb.append("null"); + else + sb.append("name:").append(getName()) + .append(" wsname:").append(getWorkspaceName()); + + return sb.toString(); + } +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTCoverageStoreList.java b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTCoverageStoreList.java new file mode 100644 index 0000000..84e4e18 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTCoverageStoreList.java @@ -0,0 +1,61 @@ +/* + * 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.decoder; + +import it.geosolutions.geoserver.rest.decoder.utils.NameLinkElem; +import it.geosolutions.geoserver.rest.decoder.utils.JDOMBuilder; +import org.jdom.Element; + +/** + * Parses list of summary data about CoverageStores. + * + *

This is the XML REST representation: + *

{@code 
+  
+    sfdem
+    
+  
+
+ *
+}
+ * + * @author ETj (etj at geo-solutions.it) + */ +public class RESTCoverageStoreList extends RESTAbstractList { + + public static RESTCoverageStoreList build(String response) { + Element elem = JDOMBuilder.buildElement(response); + return elem == null? null : new RESTCoverageStoreList(elem); + } + + protected RESTCoverageStoreList(Element list) { + super(list); + } + +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTDataStore.java b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTDataStore.java new file mode 100644 index 0000000..22c24f0 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTDataStore.java @@ -0,0 +1,133 @@ +/* + * 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.decoder; + +import it.geosolutions.geoserver.rest.decoder.utils.JDOMBuilder; +import java.util.List; +import org.jdom.Element; + +/** + * Parse DataStores returned as XML REST objects. + *

+ * This is the XML document returned by GeoServer when requesting a DataStore: + *

+ * {@code
+
+    sf
+    true
+    
+        sf
+        
+    
+    
+        http://www.openplans.org/spearfish
+        file:data/sf
+    
+    
+        
+    
+
+ * }
+ * 
+ * Note: the whole XML fragment is stored in memory. At the moment, there are + * methods to retrieve only the more useful data. + * @author etj + */ +public class RESTDataStore { + + private final Element dsElem; + + public enum DBType { + + POSTGIS("postgis"), + SHP("shp"), + UNKNOWN(null); + private final String restName; + + private DBType(String restName) { + this.restName = restName; + } + + public static DBType get(String restName) { + for (DBType type : values()) { + if (type == UNKNOWN) { + continue; + } + if (type.restName.equals(restName)) { + return type; + } + } + return UNKNOWN; + } + }; + + public static RESTDataStore build(String xml) { + if (xml == null) { + return null; + } + + Element e = JDOMBuilder.buildElement(xml); + if (e != null) { + return new RESTDataStore(e); + } else { + return null; + } + } + + protected RESTDataStore(Element dsElem) { + this.dsElem = dsElem; + } + + public String getName() { + return dsElem.getChildText("name"); + } + + public String getWorkspaceName() { + return dsElem.getChild("workspace").getChildText("name"); + } + + 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(); + } + } + } + + return null; + } + + public DBType getType() { + return DBType.get(getConnectionParameter("dbtype")); + } +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTDataStoreList.java b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTDataStoreList.java new file mode 100644 index 0000000..8ebef91 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTDataStoreList.java @@ -0,0 +1,47 @@ +/* + * 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.decoder; + +import it.geosolutions.geoserver.rest.decoder.utils.NameLinkElem; +import it.geosolutions.geoserver.rest.decoder.utils.JDOMBuilder; +import org.jdom.Element; + +/** + * Parses list of summary data about Datastores. + * + * @author ETj (etj at geo-solutions.it) + */ +public class RESTDataStoreList extends RESTAbstractList { + + public static RESTDataStoreList build(String response) { + Element elem = JDOMBuilder.buildElement(response); + return elem == null? null : new RESTDataStoreList(elem); + } + + protected RESTDataStoreList(Element list) { + super(list); + } +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTFeatureType.java b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTFeatureType.java new file mode 100644 index 0000000..38a8d6b --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTFeatureType.java @@ -0,0 +1,246 @@ +/* + * 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.decoder; + +import it.geosolutions.geoserver.rest.decoder.utils.JDOMBuilder; +import it.geosolutions.geoserver.rest.decoder.utils.JDOMListIterator; +import java.util.Iterator; +import org.jdom.Element; + +/** + * Parse FeatureTypes returned as XML REST objects. + * + *

This is the XML REST representation: + *

+ * {@code
+
+  tasmania_cities
+  tasmania_cities
+  
+    topp
+    
+  
+  Tasmania cities
+  Cities in Tasmania (actually, just the capital)
+  
+    cities
+    Tasmania
+  
+  GEOGCS["GCS_WGS_1984",
+  DATUM["WGS_1984",
+    SPHEROID["WGS_1984", 6378137.0, 298.257223563]],
+  PRIMEM["Greenwich", 0.0],
+  UNIT["degree", 0.017453292519943295],
+  AXIS["Longitude", EAST],
+  AXIS["Latitude", NORTH]]
+  EPSG:4326
+  
+    147.2910004483
+    147.2910004483
+    -42.851001816890005
+    -42.851001816890005
+    EPSG:4326
+  
+  
+    145.19754
+    148.27298000000002
+    -43.423512
+    -40.852802
+    EPSG:4326
+  
+  FORCE_DECLARED
+  true
+  
+    3600
+    false
+    10
+    true
+    tasmania_cities
+  
+  
+    taz_shapes
+    
+  
+  
+    
+      the_geom
+      0
+      1
+      false
+    
+    
+      CITY_NAME
+      0
+      1
+      false
+    
+    
+      ADMIN_NAME
+      0
+      1
+      false
+    
+    
+      CNTRY_NAME
+      0
+      1
+      false
+    
+    
+      STATUS
+      0
+      1
+      false
+    
+    
+      POP_CLASS
+      0
+      1
+      false
+    
+  
+  0
+  0
+
+ * }
+ * @author etj + */ +public class RESTFeatureType extends RESTResource { + + public static class Attribute { + private String name; + private String binding; + + public String getBinding() { + return binding; + } + + public void setBinding(String binding) { + this.binding = binding; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + public static RESTFeatureType build(String response) { + Element elem = JDOMBuilder.buildElement(response); + return elem == null? null : new RESTFeatureType(elem); + } + + public RESTFeatureType(Element resource) { + super(resource); + } + + public RESTFeatureType(RESTResource resource) { + super(resource.rootElem); + } + +// public String getName() { +// return rootElem.getChildText("name"); +// } + +// public String getNativeName() { +// return rootElem.getChildText("nativeName"); +// } + +// public String getNameSpace() { +// return rootElem.getChild("namespace").getChildText("name"); +// } + +// public String getStoreName() { +// return rootElem.getChild("store").getChildText("name"); +// } +// +// public String getStoreType() { +// return rootElem.getChild("store").getAttributeValue("class"); +// } + + /** + * Get the URL to retrieve the featuretype. + *
{@code
+        
+        tasmania_cities
+        
+    
+     * }
+     */
+//    public String getStoreUrl() {
+//		Element store = rootElem.getChild("store");
+//        Element atom = store.getChild("link", Namespace.getNamespace("atom", "http://www.w3.org/2005/Atom"));
+//        return atom.getAttributeValue("href");
+//    }
+
+//	public String getCRS() {
+//		Element elBBox = rootElem.getChild("latLonBoundingBox");
+//		return elBBox.getChildText("crs");
+//	}
+//
+//	protected double getLatLonEdge(String edge) {
+//		Element elBBox = rootElem.getChild("latLonBoundingBox");
+//		return Double.parseDouble(elBBox.getChildText(edge));
+//	}
+//
+//	public double getMinX() {
+//		return getLatLonEdge("minx");
+//	}
+//	public double getMaxX() {
+//		return getLatLonEdge("maxx");
+//	}
+//	public double getMinY() {
+//		return getLatLonEdge("miny");
+//	}
+//	public double getMaxY() {
+//		return getLatLonEdge("maxy");
+//	}
+
+    public Iterable getAttributes() {
+
+        return new Iterable() {
+            public Iterator iterator() {
+                return attributesIterator();
+            }
+        };
+    }
+
+    public Iterator attributesIterator() {
+        Element attrs = rootElem.getChild("attributes");
+        return new JDOMListIterator(attrs.getChildren()) {
+            @Override
+            public Attribute transform(Element listItem) {
+                Attribute ret = new Attribute();
+                ret.setName(listItem.getChildText("name"));
+                ret.setBinding(listItem.getChildText("binding"));
+                return ret;
+            }
+        };
+    }
+}
diff --git a/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTLayer.java b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTLayer.java
new file mode 100644
index 0000000..c194dbe
--- /dev/null
+++ b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTLayer.java
@@ -0,0 +1,180 @@
+/*
+ *  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.decoder;
+
+import it.geosolutions.geoserver.rest.decoder.utils.JDOMBuilder;
+import org.jdom.Element;
+import org.jdom.Namespace;
+
+/**
+ * Parse Layers returned as XML REST objects.
+ *
+ * 

This is the XML REST representation: + *

+ * {@code
+
+    tasmania_cities
+    /
+    VECTOR
+    
+        capitals
+        
+    
+    
+        tasmania_cities
+        
+    
+    true
+    
+        0
+        0
+    
+
+ * }
+ * @author etj + */ +public class RESTLayer { + private final Element layerElem; + + public enum TYPE { + VECTOR("VECTOR"), + RASTER("RASTER"), + UNKNOWN(null); + + private final String restName; + + private TYPE(String restName) { + this.restName = restName; + } + + public static TYPE get(String restName) { + for (TYPE type : values()) { + if(type == UNKNOWN) + continue; + if(type.restName.equals(restName)) + return type; + } + return UNKNOWN; + } + }; + + public static RESTLayer build(String response) { + if(response == null) + return null; + + Element pb = JDOMBuilder.buildElement(response); + if(pb != null) + return new RESTLayer(pb); + else + return null; + } + + public RESTLayer(Element layerElem) { + this.layerElem = layerElem; + } + + public String getName() { + return layerElem.getChildText("name"); + } + + public String getTypeString() { + return layerElem.getChildText("type"); + } + + public TYPE getType() { + return TYPE.get(getTypeString()); + } + + public String getDefaultStyle() { + Element defaultStyle = layerElem.getChild("defaultStyle"); + return defaultStyle == null? null : defaultStyle.getChildText("name"); + } + + public String getTitle() { + Element resource = layerElem.getChild("resource"); + return resource.getChildText("title"); + } + + public String getAbstract() { + Element resource = layerElem.getChild("resource"); + return resource.getChildText("abstract"); + } + + public String getNameSpace() { + Element resource = layerElem.getChild("resource"); + return resource.getChild("namespace").getChildText("name"); + } + +// public String getStoreName() { +// Element resource = layerElem.getChild("resource"); +// return resource.getChild("store").getChildText("name"); +// } +// +// public String getStoreType() { +// Element resource = layerElem.getChild("resource"); +// return resource.getChild("store").getAttributeValue("class"); +// } + +// public String getCRS() { +// Element resource = layerElem.getChild("resource"); +// Element elBBox = resource.getChild("latLonBoundingBox"); +// return elBBox.getChildText("crs"); +// } + + /** + * Get the URL to retrieve the featuretype. + *
{@code
+        
+        tasmania_cities
+        
+    
+     * }
+     */
+    public String getResourceUrl() {
+		Element resource = layerElem.getChild("resource");
+        Element atom = resource.getChild("link", Namespace.getNamespace("atom", "http://www.w3.org/2005/Atom"));
+        return atom.getAttributeValue("href");
+    }
+
+//	protected double getLatLonEdge(String edge) {
+//		Element resource = layerElem.getChild("resource");
+//		Element elBBox = resource.getChild("latLonBoundingBox");
+//		return Double.parseDouble(elBBox.getChildText(edge));
+//	}
+//
+//	public double getMinX() {
+//		return getLatLonEdge("minx");
+//	}
+//	public double getMaxX() {
+//		return getLatLonEdge("maxx");
+//	}
+//	public double getMinY() {
+//		return getLatLonEdge("miny");
+//	}
+//	public double getMaxY() {
+//		return getLatLonEdge("maxy");
+//	}
+}
diff --git a/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTLayerGroup.java b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTLayerGroup.java
new file mode 100644
index 0000000..0587156
--- /dev/null
+++ b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTLayerGroup.java
@@ -0,0 +1,120 @@
+/*
+ *  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.decoder;
+
+import it.geosolutions.geoserver.rest.decoder.utils.JDOMBuilder;
+import org.jdom.Element;
+
+/**
+ * Parse LayerGroups returned as XML REST objects.
+ *
+ * 

This is the XML REST representation: + *

+ * {@code
+
+  tasmania
+  
+    
+      tasmania_state_boundaries
+      
+    
+    
+      tasmania_water_bodies
+      
+    
+    
+      tasmania_roads
+      
+    
+    
+      tasmania_cities
+      
+    
+  
+  
+    
+      
+      
+}
+ * + * @author ETj (etj at geo-solutions.it) + */ +public class RESTStyleList extends RESTAbstractList { + + public static RESTStyleList build(String response) { + Element elem = JDOMBuilder.buildElement(response); + return elem == null? null : new RESTStyleList(elem); + } + + protected RESTStyleList(Element list) { + super(list); + } + +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTWorkspaceList.java b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTWorkspaceList.java new file mode 100644 index 0000000..611f31d --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/decoder/RESTWorkspaceList.java @@ -0,0 +1,128 @@ +/* + * 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.decoder; + +import it.geosolutions.geoserver.rest.decoder.utils.NameLinkElem; +import it.geosolutions.geoserver.rest.decoder.utils.JDOMBuilder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.jdom.Element; + +/** + * Parses list of summary data about Workspaces. + *
Single items are handled by {@link RESTShortWorkspace}. + + * @author ETj (etj at geo-solutions.it) + */ +public class RESTWorkspaceList implements Iterable { + + private final List wsList; + + public static RESTWorkspaceList build(String response) { + if(response == null) + return null; + + Element elem = JDOMBuilder.buildElement(response); + if(elem != null) + return new RESTWorkspaceList(elem); + else + return null; + } + + protected RESTWorkspaceList(Element wslistroot) { + List tmpList = new ArrayList(); + for (Element wselem : (List) wslistroot.getChildren("workspace")) { + tmpList.add(wselem); + } + + wsList = Collections.unmodifiableList(tmpList); + } + + public int size() { + return wsList.size(); + } + + public boolean isEmpty() { + return wsList.isEmpty(); + } + + public RESTShortWorkspace get(int index) { + return new RESTShortWorkspace(wsList.get(index)); + } + + public Iterator iterator() { + return new RESTWSListIterator(wsList); + } + + + private static class RESTWSListIterator implements Iterator { + + private final Iterator iter; + + public RESTWSListIterator(List orig) { + iter = orig.iterator(); + } + + public boolean hasNext() { + return iter.hasNext(); + } + + public RESTShortWorkspace next() { + return new RESTShortWorkspace(iter.next()); + } + + public void remove() { + throw new UnsupportedOperationException("Not supported."); + } + } + + /** + * Workspace summary info. + *
This is an XML fragment: + *
+     * {@code
+     *   
+     *      it.geosolutions
+     *      
+     *  
+     * }
+     * 
+ */ + + public static class RESTShortWorkspace extends NameLinkElem { + + public RESTShortWorkspace(Element elem) { + super(elem); + } + } + +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/decoder/package.html b/src/main/java/it/geosolutions/geoserver/rest/decoder/package.html new file mode 100644 index 0000000..869793b --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/decoder/package.html @@ -0,0 +1,5 @@ + +Decoders for GeoServer's beans. +Only some getters are available; there were developed only the most used getters, +in order not to replicate the whole GeoServer beans hierarchy. + \ No newline at end of file diff --git a/src/main/java/it/geosolutions/geoserver/rest/decoder/utils/JDOMBuilder.java b/src/main/java/it/geosolutions/geoserver/rest/decoder/utils/JDOMBuilder.java new file mode 100644 index 0000000..e3efb62 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/decoder/utils/JDOMBuilder.java @@ -0,0 +1,61 @@ +/* + * 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.decoder.utils; + +import java.io.IOException; +import java.io.StringReader; +import org.apache.log4j.Logger; +import org.jdom.Document; +import org.jdom.Element; +import org.jdom.JDOMException; +import org.jdom.input.SAXBuilder; + +/** + * + * @author ETj (etj at geo-solutions.it) + */ +public class JDOMBuilder { + + private final static Logger LOGGER = Logger.getLogger(JDOMBuilder.class); + + public static Element buildElement(String response) { + if(response == null) + return null; + + try{ + SAXBuilder builder = new SAXBuilder(); + Document doc = builder.build(new StringReader(response)); + return doc.getRootElement(); + } catch (JDOMException ex) { + LOGGER.warn("Ex parsing response", ex); + } catch (IOException ex) { + LOGGER.warn("Ex loading response", ex); + } + + return null; + } + +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/decoder/utils/JDOMListIterator.java b/src/main/java/it/geosolutions/geoserver/rest/decoder/utils/JDOMListIterator.java new file mode 100644 index 0000000..b36272c --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/decoder/utils/JDOMListIterator.java @@ -0,0 +1,57 @@ +/* + * 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.decoder.utils; + +import java.util.Iterator; +import java.util.List; +import org.jdom.Element; + +/** + * + * @author ETj (etj at geo-solutions.it) + */ +public abstract class JDOMListIterator implements Iterator { + + private final Iterator iter; + + public JDOMListIterator(List orig) { + iter = orig.iterator(); + } + + public boolean hasNext() { + return iter.hasNext(); + } + + public ELEM next() { + return transform(iter.next()); + } + + public abstract ELEM transform(Element listItem); + + public void remove() { + throw new UnsupportedOperationException("Not supported."); + } +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/decoder/utils/NameLinkElem.java b/src/main/java/it/geosolutions/geoserver/rest/decoder/utils/NameLinkElem.java new file mode 100644 index 0000000..0c77393 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/decoder/utils/NameLinkElem.java @@ -0,0 +1,45 @@ +/* + * 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.decoder.utils; + +import org.jdom.Element; + +/** + * + * @author ETj (etj at geo-solutions.it) + */ +public class NameLinkElem { + private final Element elem; + + public NameLinkElem(Element elem) { + this.elem = elem; + } + + public String getName() { + return elem.getChildText("name"); + } + +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/decoder/utils/package.html b/src/main/java/it/geosolutions/geoserver/rest/decoder/utils/package.html new file mode 100644 index 0000000..ceb81b2 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/decoder/utils/package.html @@ -0,0 +1,3 @@ + +Some util classes. + \ No newline at end of file diff --git a/src/main/java/it/geosolutions/geoserver/rest/encoder/GSCoverageEncoder.java b/src/main/java/it/geosolutions/geoserver/rest/encoder/GSCoverageEncoder.java new file mode 100644 index 0000000..542c644 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/encoder/GSCoverageEncoder.java @@ -0,0 +1,42 @@ +/* + * 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; + +/** + * + * @author ETj (etj at geo-solutions.it) + */ +public class GSCoverageEncoder extends PropertyXMLEncoder { + + public GSCoverageEncoder() { + super("coverage"); + set("enabled", "true"); + } + + public void setSRS(String srs) { + setOrRemove("srs", srs); + } +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/encoder/GSFeatureTypeEncoder.java b/src/main/java/it/geosolutions/geoserver/rest/encoder/GSFeatureTypeEncoder.java new file mode 100644 index 0000000..8e6ae0c --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/encoder/GSFeatureTypeEncoder.java @@ -0,0 +1,51 @@ +/* + * 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; + +/** + * + * @author ETj (etj at geo-solutions.it) + */ +public class GSFeatureTypeEncoder extends PropertyXMLEncoder { + + public GSFeatureTypeEncoder() { + super("featureType"); + set("enabled", "true"); + } + + public void setName(String name) { + setOrRemove("name", name); + } + + public void setSRS(String srs) { + setOrRemove("srs", srs); + } + +// public void setNativeCRS(String crs) { +// setOrRemove("nativeCRS", crs); +// } + +} \ No newline at end of file diff --git a/src/main/java/it/geosolutions/geoserver/rest/encoder/GSLayerEncoder.java b/src/main/java/it/geosolutions/geoserver/rest/encoder/GSLayerEncoder.java new file mode 100644 index 0000000..0ae7c83 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/encoder/GSLayerEncoder.java @@ -0,0 +1,47 @@ +/* + * 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; + +/** + * + * @author ETj (etj at geo-solutions.it) + */ +public class GSLayerEncoder extends PropertyXMLEncoder { + + public GSLayerEncoder() { + super("layer"); + set("enabled", "true"); + } + + public void setWmsPath(String wmspath) { + setOrRemove("wmspath", wmspath); + } + + public void setDefaultStyle(String defaultStyle) { + setOrRemove("defaultStyle", defaultStyle); + } + +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/encoder/GSWorkspaceEncoder.java b/src/main/java/it/geosolutions/geoserver/rest/encoder/GSWorkspaceEncoder.java new file mode 100644 index 0000000..350a9ce --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/encoder/GSWorkspaceEncoder.java @@ -0,0 +1,47 @@ +/* + * 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; + +/** + * + * @author ETj (etj at geo-solutions.it) + */ +public class GSWorkspaceEncoder extends PropertyXMLEncoder { + + public GSWorkspaceEncoder() { + super("workspace"); + } + + public GSWorkspaceEncoder(String name) { + this(); + setName(name); + } + + public void setName(String name) { + setOrRemove("name", name); + } + +} \ No newline at end of file diff --git a/src/main/java/it/geosolutions/geoserver/rest/encoder/PropertyXMLEncoder.java b/src/main/java/it/geosolutions/geoserver/rest/encoder/PropertyXMLEncoder.java new file mode 100644 index 0000000..a17e655 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/encoder/PropertyXMLEncoder.java @@ -0,0 +1,101 @@ +/* + * 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 java.util.HashMap; +import java.util.Map; +import org.jdom.Element; +import org.jdom.output.Format; +import org.jdom.output.XMLOutputter; + +/** + * Creates an XML document by mapping properties to XML nodes.
+ * You can set the root element in the constructor. + * Any key/value pair will be encoded as value node. + * Any key containing one or more slash ("/") will be encoded as nested nodes; + *

e.g.:

+ *
 {@code key = "k1/k2/k3", value = "value" }
+ * will be encoded as + *
 {@code value }
+ * + * @author ETj (etj at geo-solutions.it) + */ +public class PropertyXMLEncoder { + + private final static XMLOutputter OUTPUTTER = new XMLOutputter(Format.getCompactFormat()); + private final Map configElements = new HashMap(); + private final String rootName; + + public PropertyXMLEncoder(String rootName) { + this.rootName = rootName; + } + + protected void setOrRemove(String key, String value) { + if (value != null) { + configElements.put(key, value); + } else { + configElements.remove(key); + } + } + + protected void set(String key, String value) { + setOrRemove(key, value); + } + + public boolean isEmpty() { + return configElements.isEmpty(); + } + + public String encodeXml() { + + Element layer = new Element(rootName); + for (String key : configElements.keySet()) { + final String value = configElements.get(key); + add(layer, key, value); + } + + return OUTPUTTER.outputString(layer); + } + + private void add(Element e, String key, String value) { + if( ! key.contains("/") ) { + e.addContent(new Element(key).setText(value)); + } else { + int i = key.indexOf("/"); + String childName = key.substring(0,i); + String newkey = key.substring(i+1); + + Element child = e.getChild(childName); + if(child == null) { + child = new Element(childName); + e.addContent(child); + } + + add(child, newkey, value); + } + + } +} diff --git a/src/main/java/it/geosolutions/geoserver/rest/encoder/package.html b/src/main/java/it/geosolutions/geoserver/rest/encoder/package.html new file mode 100644 index 0000000..0ef2892 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/encoder/package.html @@ -0,0 +1,5 @@ + +Encoders for GeoServer's beans. +Only some setters are available; there were developed only the most used setters, +in order not to replicate the whole GeoServer beans hierarchy. + \ No newline at end of file diff --git a/src/main/java/it/geosolutions/geoserver/rest/package.html b/src/main/java/it/geosolutions/geoserver/rest/package.html new file mode 100644 index 0000000..e6cf841 --- /dev/null +++ b/src/main/java/it/geosolutions/geoserver/rest/package.html @@ -0,0 +1,3 @@ + +Main GSManager classes are here. + \ No newline at end of file diff --git a/src/test/java/it/geosolutions/geoserver/rest/ConfigTest.java b/src/test/java/it/geosolutions/geoserver/rest/ConfigTest.java new file mode 100644 index 0000000..73aa895 --- /dev/null +++ b/src/test/java/it/geosolutions/geoserver/rest/ConfigTest.java @@ -0,0 +1,114 @@ +/* + * 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; + + +import it.geosolutions.geoserver.rest.decoder.RESTCoverageStore; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FilenameFilter; +import java.io.IOException; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.filefilter.SuffixFileFilter; +import org.apache.log4j.Logger; +import org.springframework.core.io.ClassPathResource; + +/** + * Testcase for publishing layers on geoserver. + * We need a running GeoServer to properly run the tests. + * Login credentials are hardcoded at the moment (localhost:8080 admin/geoserver). + * If such geoserver instance cannot be contacted, tests will be skipped. + * + * + * @author etj + */ +public class ConfigTest extends GeoserverRESTTest { + private final static Logger LOGGER = Logger.getLogger(ConfigTest.class); + + private static final String DEFAULT_WS = "it.geosolutions"; + + + public ConfigTest(String testName) { + super(testName); + } + + public void testEtj() throws FileNotFoundException, IOException { + if (!enabled()) return; + deleteAll(); + + assertTrue(reader.getWorkspaces().isEmpty()); + assertTrue(publisher.createWorkspace(DEFAULT_WS)); + + insertStyles(); + insertExternalGeotiff(); + insertExternalShape(); + + boolean ok = publisher.publishDBLayer(DEFAULT_WS, "pg_kids", "easia_gaul_0_aggr", "EPSG:4326", "default_polygon"); +// assertTrue(ok); + } + + public void insertStyles() throws FileNotFoundException, IOException { + + File sldDir = new ClassPathResource("testdata").getFile(); + for(File sldFile : sldDir.listFiles((FilenameFilter)new SuffixFileFilter(".sld"))) { + LOGGER.info("Existing styles: " + reader.getStyles().getNames()); + String basename = FilenameUtils.getBaseName(sldFile.toString()); + LOGGER.info("Publishing style " + sldFile + " as " + basename); + assertTrue("Cound not publish " + sldFile, publisher.publishStyle(sldFile, basename)); + } + } + + + public void insertExternalGeotiff() throws FileNotFoundException, IOException { + + String storeName = "testRESTStoreGeotiff"; + String layerName = "resttestdem"; + + File geotiff = new ClassPathResource("testdata/resttestdem.tif").getFile(); + RESTCoverageStore pc = publisher.publishExternalGeoTIFF(DEFAULT_WS, storeName, geotiff, null, null); + + assertNotNull(pc); + } + + public void insertExternalShape() throws FileNotFoundException, IOException { + + File zipFile = new ClassPathResource("testdata/resttestshp.zip").getFile(); + + boolean published = publisher.publishShp(DEFAULT_WS, "anyname", "cities", zipFile, "EPSG:4326", "default_point"); + assertTrue("publish() failed", published); + + + //test delete + boolean ok = publisher.unpublishFeatureType(DEFAULT_WS, "anyname", "cities"); + assertTrue("Unpublish() failed", ok); + // remove also datastore + boolean dsRemoved = publisher.removeDatastore(DEFAULT_WS, "anyname"); + assertTrue("removeDatastore() failed", dsRemoved); + } + +} \ No newline at end of file diff --git a/src/test/java/it/geosolutions/geoserver/rest/GeoserverRESTPublisherTest.java b/src/test/java/it/geosolutions/geoserver/rest/GeoserverRESTPublisherTest.java new file mode 100644 index 0000000..e83d082 --- /dev/null +++ b/src/test/java/it/geosolutions/geoserver/rest/GeoserverRESTPublisherTest.java @@ -0,0 +1,334 @@ +/* + * 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; + +import it.geosolutions.geoserver.rest.decoder.RESTLayer; +import it.geosolutions.geoserver.rest.decoder.RESTCoverageStore; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.apache.commons.io.IOUtils; +import org.apache.log4j.Logger; +import org.springframework.core.io.ClassPathResource; + +/** + * Testcase for publishing layers on geoserver. + * We need a running GeoServer to properly run the tests. + * If such geoserver instance cannot be contacted, tests will be skipped. + * + * @author etj + */ +public class GeoserverRESTPublisherTest extends GeoserverRESTTest { + + private final static Logger LOGGER = Logger.getLogger(GeoserverRESTPublisherTest.class); + private static final String DEFAULT_WS = "it.geosolutions"; + + public GeoserverRESTPublisherTest(String testName) { + super(testName); + } + + public void testWorkspaces() { + if (!enabled()) return; + deleteAll(); + + assertEquals(0, reader.getWorkspaces().size()); + + assertTrue(publisher.createWorkspace("WS1")); + assertTrue(publisher.createWorkspace("WS2")); + assertEquals(2, reader.getWorkspaces().size()); + + assertFalse(publisher.createWorkspace("WS2")); + assertEquals(2, reader.getWorkspaces().size()); + } + + public void testStyles() throws IOException { + if (!enabled()) return; + deleteAll(); + + assertEquals(0, reader.getStyles().size()); + + final String styleName = "restteststyle"; + File sldFile = new ClassPathResource("testdata/restteststyle.sld").getFile(); + + // insert style + assertTrue(publisher.publishStyle(sldFile)); + assertTrue(reader.existsStyle(styleName)); + + assertFalse(publisher.publishStyle(sldFile)); + assertTrue(reader.existsStyle(styleName)); + + String sld = reader.getSLD(styleName); + assertNotNull(sld); + assertEquals(1475, sld.length()); + + assertEquals(1, reader.getStyles().size()); + } + + public void testExternalGeotiff() throws FileNotFoundException, IOException { + if (!enabled()) return; + deleteAll(); + + String storeName = "testRESTStoreGeotiff"; + String layerName = "resttestdem"; + + assertTrue(reader.getWorkspaces().isEmpty()); + assertTrue(publisher.createWorkspace(DEFAULT_WS)); + + File geotiff = new ClassPathResource("testdata/resttestdem.tif").getFile(); + + // known state? + assertFalse("Cleanup failed", existsLayer(layerName)); + + // test insert + RESTCoverageStore pc = publisher.publishExternalGeoTIFF(DEFAULT_WS, storeName, geotiff, null, null); + assertNotNull("publish() failed", pc); + assertTrue(existsLayer(layerName)); + LOGGER.info(pc); + RESTCoverageStore reloadedCS = reader.getCoverageStore(DEFAULT_WS, storeName); + + assertEquals(pc.getName(), reloadedCS.getName()); + assertEquals(pc.getWorkspaceName(), reloadedCS.getWorkspaceName()); + + //test delete + assertTrue("Unpublish() failed", publisher.unpublishCoverage(DEFAULT_WS, storeName, layerName)); + assertTrue("Unpublish() failed", publisher.removeCoverageStore(DEFAULT_WS, storeName)); + assertFalse("Bad unpublish()", publisher.unpublishCoverage(DEFAULT_WS, storeName, layerName)); + assertFalse(existsLayer(layerName)); + } + + protected void cleanupTestFT(String layerName, String ns, String storeName) { + // dry run delete to work in a known state + RESTLayer testLayer = reader.getLayer(layerName); + if (testLayer != null) { + LOGGER.info("Clearing stale test layer " + layerName); + boolean ok = publisher.unpublishFeatureType(ns, storeName, layerName); + if (!ok) { + fail("Could not unpublish layer " + layerName); + } + } + if (publisher.removeDatastore(ns, storeName)) { + LOGGER.info("Cleared stale datastore " + storeName); + } + assertFalse("Cleanup failed", existsLayer(layerName)); + } + + protected void cleanupTestStyle(final String styleName) { + // dry run delete to work in a known state + if (reader.existsStyle(styleName)) { + LOGGER.info("Clearing stale test style " + styleName); + boolean ok = publisher.removeStyle(styleName); + if (!ok) { + fail("Could not unpublish style " + styleName); + } + } + assertFalse("Cleanup failed", reader.existsStyle(styleName)); + } + + public void testPublishDeleteShapeZip() throws FileNotFoundException, IOException { + if (!enabled()) { + return; + } +// Assume.assumeTrue(enabled); + + String ns = "it.geosolutions"; + String storeName = "resttestshp"; + String layerName = "cities"; + + File zipFile = new ClassPathResource("testdata/resttestshp.zip").getFile(); + + // known state? + cleanupTestFT(layerName, ns, storeName); + + // test insert + boolean published = publisher.publishShp(ns, storeName, layerName, zipFile); + assertTrue("publish() failed", published); + assertTrue(existsLayer(layerName)); + + RESTLayer layer = reader.getLayer(layerName); + + LOGGER.info("Layer style is " + layer.getDefaultStyle()); + + //test delete + boolean ok = publisher.unpublishFeatureType(ns, storeName, layerName); + assertTrue("Unpublish() failed", ok); + assertFalse(existsLayer(layerName)); + + // remove also datastore + boolean dsRemoved = publisher.removeDatastore(ns, storeName); + assertTrue("removeDatastore() failed", dsRemoved); + + } + + public void testPublishDeleteStyledShapeZip() throws FileNotFoundException, IOException { + if (!enabled()) { + return; + } +// Assume.assumeTrue(enabled); + + String ns = "it.geosolutions"; + String storeName = "resttestshp"; + String layerName = "cities"; + + File zipFile = new ClassPathResource("testdata/resttestshp.zip").getFile(); + cleanupTestFT(layerName, ns, storeName); + + final String styleName = "restteststyle"; + File sldFile = new ClassPathResource("testdata/restteststyle.sld").getFile(); + cleanupTestStyle(styleName); + + // insert style + boolean sldpublished = publisher.publishStyle(sldFile); // Will take the name from sld contents + assertTrue("style publish() failed", sldpublished); + assertTrue(reader.existsStyle(styleName)); + + // test insert + boolean published = publisher.publishShp(ns, storeName, layerName, zipFile, "EPSG:4326", styleName); + assertTrue("publish() failed", published); + assertTrue(existsLayer(layerName)); + + RESTLayer layer = reader.getLayer(layerName); +// RESTLayer layerDecoder = new RESTLayer(layer); + LOGGER.info("Layer style is " + layer.getDefaultStyle()); + assertEquals("Style not assigned properly", styleName, layer.getDefaultStyle()); + + //test delete + boolean ok = publisher.unpublishFeatureType(ns, storeName, layerName); + assertTrue("Unpublish() failed", ok); + assertFalse(existsLayer(layerName)); + + // remove also datastore + boolean dsRemoved = publisher.removeDatastore(ns, storeName); + assertTrue("removeDatastore() failed", dsRemoved); + + //test delete style + boolean oksld = publisher.removeStyle(styleName); + assertTrue("Unpublish() failed", oksld); + assertFalse(reader.existsStyle(styleName)); + } + + public void testPublishDeleteStyleFile() throws FileNotFoundException, IOException { + if (!enabled()) { + return; + } +// Assume.assumeTrue(enabled); + final String styleName = "restteststyle"; + + File sldFile = new ClassPathResource("testdata/restteststyle.sld").getFile(); + + // known state? + cleanupTestStyle(styleName); + + // test insert + boolean published = publisher.publishStyle(sldFile); // Will take the name from sld contents + assertTrue("publish() failed", published); + assertTrue(reader.existsStyle(styleName)); + + //test delete + boolean ok = publisher.removeStyle(styleName); + assertTrue("Unpublish() failed", ok); + assertFalse(reader.existsStyle(styleName)); + } + + public void testPublishDeleteStyleString() throws FileNotFoundException, IOException { + if (!enabled()) { + return; + } +// Assume.assumeTrue(enabled); + final String styleName = "restteststyle"; + + File sldFile = new ClassPathResource("testdata/restteststyle.sld").getFile(); + + // known state? + cleanupTestStyle(styleName); + + // test insert + String sldContent = IOUtils.toString(new FileInputStream(sldFile)); + + boolean published = publisher.publishStyle(sldContent); // Will take the name from sld contents + assertTrue("publish() failed", published); + assertTrue(reader.existsStyle(styleName)); + + //test delete + boolean ok = publisher.removeStyle(styleName); + assertTrue("Unpublish() failed", ok); + assertFalse(reader.existsStyle(styleName)); + } + + public void testDeleteUnexistingCoverage() throws FileNotFoundException, IOException { + if (!enabled()) { + return; + } +// Assume.assumeTrue(enabled); + + String wsName = "this_ws_does_not_exist"; + String storeName = "this_store_does_not_exist"; + String layerName = "this_layer_does_not_exist"; + + boolean ok = publisher.unpublishCoverage(wsName, storeName, layerName); + assertFalse("unpublished not existing layer", ok); + } + + public void testDeleteUnexistingFeatureType() throws FileNotFoundException, IOException { + if (!enabled()) { + return; + } +// Assume.assumeTrue(enabled); + + String wsName = "this_ws_does_not_exist"; + String storeName = "this_store_does_not_exist"; + String layerName = "this_layer_does_not_exist"; + + boolean ok = publisher.unpublishFeatureType(wsName, storeName, layerName); + assertFalse("unpublished not existing layer", ok); + } + + public void testDeleteUnexistingDatastore() throws FileNotFoundException, IOException { + if (!enabled()) { + return; + } +// Assume.assumeTrue(enabled); + + String wsName = "this_ws_does_not_exist"; + String storeName = "this_store_does_not_exist"; + + boolean ok = publisher.removeDatastore(wsName, storeName); + assertFalse("removed not existing datastore", ok); + } + +// public void testDeleteUnexistingFT() throws FileNotFoundException, IOException { +// String wsName = "this_ws_does_not_exist"; +// String storeName = "this_store_does_not_exist"; +// String layerName = "this_layer_does_not_exist"; +// +// boolean ok = publisher.unpublishFT(wsName, storeName, layerName); +// assertFalse("unpublished not existing layer", ok); +// } + private boolean existsLayer(String layername) { + return reader.getLayer(layername) != null; + } +} diff --git a/src/test/java/it/geosolutions/geoserver/rest/GeoserverRESTReaderTest.java b/src/test/java/it/geosolutions/geoserver/rest/GeoserverRESTReaderTest.java new file mode 100644 index 0000000..d92739c --- /dev/null +++ b/src/test/java/it/geosolutions/geoserver/rest/GeoserverRESTReaderTest.java @@ -0,0 +1,203 @@ +/* + * 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; + +import it.geosolutions.geoserver.rest.decoder.utils.NameLinkElem; +import it.geosolutions.geoserver.rest.decoder.RESTDataStoreList; +import it.geosolutions.geoserver.rest.decoder.RESTDataStore; +import it.geosolutions.geoserver.rest.decoder.RESTLayerList; +import it.geosolutions.geoserver.rest.decoder.RESTNamespaceList; +import it.geosolutions.geoserver.rest.decoder.RESTWorkspaceList; +import java.util.List; + + +/** + * + * @author etj + */ +public class GeoserverRESTReaderTest extends GeoserverRESTTest { + + public GeoserverRESTReaderTest(String testName) { + super(testName); + } + + /** + * Test of getLayers method, of class GeoServerRESTReader. + */ + public void testGetLayers() { + if(!enabled()) return; + + RESTLayerList result = reader.getLayers(); + assertNotNull(result); +// assertEquals(/*CHANGEME*/19, result.getChildren("layer").size()); // value in default gs installation + +// System.out.println("Layers:" + result.getChildren("layer").size()); + System.out.println("Layers:" + result.size()); + System.out.print("Layers:" ); + for (NameLinkElem shlayer : result) { + assertNotNull(shlayer.getName()); + System.out.print(shlayer.getName() + " "); + } +// for (Element layer : (List)result.getChildren("layer")) { +// System.out.print(layer.getChildText("name") + " "); +// } + System.out.println(); + } + + /** + * Test of getDatastores method, of class GeoServerRESTReader. + */ + public void testGetDatastores() { + if(!enabled()) return; + + RESTWorkspaceList wslist = reader.getWorkspaces(); + assertNotNull(wslist); +// assertEquals(7, wslist.size()); // value in default gs installation + + System.out.println("Workspaces: " + wslist.size()); + int dsnum = 0; + for (RESTWorkspaceList.RESTShortWorkspace ws : wslist) { + System.out.println("Getting DSlist for WS " + ws.getName() + "..." ); + RESTDataStoreList result = reader.getDatastores(ws.getName()); + assertNotNull(result); + dsnum += result.size(); + for (NameLinkElem ds : result) { + assertNotNull(ds.getName()); + System.out.print(ds.getName() + " " ); + RESTDataStore datastore = reader.getDatastore(ws.getName(), ds.getName()); + assertNotNull(datastore); + assertEquals(ds.getName(), datastore.getName()); + assertEquals(ws.getName(), datastore.getWorkspaceName()); + } + System.out.println(); + } + System.out.println(); + System.out.println("Datastores:" + dsnum); // value in default gs installation +// assertEquals(4, dsnum); // value in default gs installation + + } + + public void testGetWSDSNames() { + if(!enabled()) + return; + + RESTWorkspaceList wslist = reader.getWorkspaces(); + assertNotNull(wslist); +// assertEquals(7, wslist.size()); // value in default gs installation + + List wsnames = reader.getWorkspaceNames(); + assertNotNull(wsnames); +// assertEquals(7, wsnames.size()); // value in default gs installation + +// System.out.println("Workspaces: " + wslist.size()); + int dsnum = 0; + int wscnt = 0; + for (RESTWorkspaceList.RESTShortWorkspace ws : wslist) { + String wsname = wsnames.get(wscnt++); + + List dsnames = reader.getDatastores(wsname).getNames(); + RESTDataStoreList dslist = reader.getDatastores(ws.getName()); + assertNotNull(dsnames); + assertNotNull(dslist); + assertEquals(dsnames.size(), dslist.size()); + } + } + + /** + * Test of getDatastore method, of class GeoServerRESTReader. + */ + public void testGetDatastore() { + //tested in testGetDatastores() + } + + /** + * Test of getLayer method, of class GeoServerRESTReader. + */ + public void testGetLayer() { + } + + /** + * Test of getNamespaceNames method, of class GeoServerRESTReader. + */ + public void testGetNamespaces() { + if(!enabled()) return; + + RESTNamespaceList result = reader.getNamespaces(); + List names = reader.getNamespaceNames(); + assertNotNull(result); +// assertEquals(7, result.size()); // value in default gs installation + assertNotNull(names); + assertEquals(names.size(), result.size()); // value in default gs installation + + System.out.println("Namespaces:" + result.size()); + System.out.print("Namespaces:" ); + int namesIdx = 0; + for (RESTNamespaceList.RESTShortNamespace ns : result) { + assertEquals("namespace mismatch", names.get(namesIdx++), ns.getName()); + System.out.print(ns.getName() + " " ); + } + System.out.println(); + } + + + /** + * Test of getWorkspaceNames method, of class GeoServerRESTReader. + */ + public void testGetWorkspaces() { + if(!enabled()) return; + + RESTWorkspaceList wslist = reader.getWorkspaces(); + assertNotNull(wslist); +// assertEquals(7, wslist.size()); // value in default gs installation + + System.out.println("Workspaces:" + wslist.size()); + System.out.print("Workspaces:"); + for (RESTWorkspaceList.RESTShortWorkspace ws : wslist) { + System.out.print(ws.getName() + " "); + } + System.out.println(); + + assertEquals(wslist.size(), reader.getWorkspaceNames().size()); + } + /** + * Test of getWorkspaceNames method, of class GeoServerRESTReader. + */ + public void testGetWorkspaceNames() { + if(!enabled()) return; + + List names = reader.getWorkspaceNames(); + assertNotNull(names); +// assertEquals(7, names.size()); // value in default gs installation + + System.out.println("Workspaces:" + names.size()); + System.out.print("Workspaces:"); + for (String name : names) { + System.out.print(name + " "); + } + System.out.println(); + } + +} diff --git a/src/test/java/it/geosolutions/geoserver/rest/GeoserverRESTTest.java b/src/test/java/it/geosolutions/geoserver/rest/GeoserverRESTTest.java new file mode 100644 index 0000000..2d50bac --- /dev/null +++ b/src/test/java/it/geosolutions/geoserver/rest/GeoserverRESTTest.java @@ -0,0 +1,257 @@ +/* + * 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; + +import it.geosolutions.geoserver.rest.decoder.utils.NameLinkElem; +import it.geosolutions.geoserver.rest.decoder.RESTCoverage; +import it.geosolutions.geoserver.rest.decoder.RESTCoverageStore; +import it.geosolutions.geoserver.rest.decoder.RESTDataStore; +import it.geosolutions.geoserver.rest.decoder.RESTFeatureType; +import it.geosolutions.geoserver.rest.decoder.RESTLayer; +import it.geosolutions.geoserver.rest.decoder.RESTLayerGroup; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +import junit.framework.TestCase; +import org.apache.log4j.Logger; + +/** + * Initializes REST params. + *

+ * Default target geoserver instance is at http://localhost:8080/geoserver. + *
Connection parameters can be customized by defining env vars + * resturl, restuser, restpw, + * + * @author etj + */ +public abstract class GeoserverRESTTest extends TestCase { + private final static Logger LOGGER = Logger.getLogger(GeoserverRESTTest.class); + + public static final String RESTURL; + public static final String RESTUSER; + public static final String RESTPW; + + public static final URL URL; + public static final GeoServerRESTReader reader; + public static final GeoServerRESTPublisher publisher; + + private static boolean enabled = false; + + static { + RESTURL = System.getProperty("resturl", "http://localhost:8080/geoserver"); + RESTUSER = System.getProperty("restuser", "admin"); + RESTPW = System.getProperty("restpw", "geoserver"); + + // These tests will destroy data, so let's make sure we do want to run them + enabled = System.getProperty("resttest", "false").equalsIgnoreCase("true"); + if( ! enabled ) + LOGGER.warn("Tests are disabled. Please read the documentation to enable them."); + + URL lurl = null; + try { + lurl = new URL(RESTURL); + } catch (MalformedURLException ex) { + } + + URL = lurl; + reader = new GeoServerRESTReader(lurl, RESTUSER, RESTPW); + publisher = new GeoServerRESTPublisher(RESTURL, RESTUSER, RESTPW); + } + + public GeoserverRESTTest(String testName) { + super(testName); + } + + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if(enabled) { + if( ! reader.existGeoserver()) { + System.out.println(getClass().getSimpleName() + ": TESTS WILL BE SKIPPED SINCE NO GEOSERVER WAS FOUND AT " + RESTURL + " ("+ RESTUSER+":"+RESTPW+")"); + enabled = false; + } else { + System.out.println(getClass().getSimpleName() + ": using geoserver instance " + RESTUSER+":"+RESTPW+ " @ " + RESTURL); + } + } + + if(enabled) + System.out.println("\n-------------------> RUNNING TEST " + this.getName()); + else + System.out.println("Skipping test " + this.getClass().getSimpleName() + "::" + this.getName()); + } + + protected boolean enabled() { + return enabled; + } + + protected void deleteAll() { + LOGGER.info("Starting DELETEALL procedure"); + deleteAllLayerGroups(); + assertTrue("Some layergroups were not removed", reader.getLayerGroups().isEmpty()); + + deleteAllLayers(); + assertTrue("Some layers were not removed", reader.getLayers().isEmpty()); + + deleteAllCoverageStores(); + deleteAllDataStores(); + + deleteAllWorkspaces(); +// assertTrue("Some workspaces were not removed", reader.getWorkspaces().isEmpty()); + + deleteAllStyles(); + assertTrue("Some styles were not removed", reader.getStyles().isEmpty()); + + LOGGER.info("ENDING DELETEALL procedure"); + } + + private void deleteAllLayerGroups() { + List groups = reader.getLayerGroups().getNames(); + LOGGER.info("Found " + groups.size() + " layerGroups"); + for (String groupName : groups) { + RESTLayerGroup group = reader.getLayerGroup(groupName); + StringBuilder sb = new StringBuilder("Group: ").append(groupName).append(":"); + for (NameLinkElem layer : group.getLayerList()) { + sb.append(" ").append(layer); + } + + boolean removed = publisher.removeLayerGroup(groupName); + LOGGER.info(sb.toString()+ ": removed: " + removed); + assertTrue("LayerGroup not removed: " + groupName, removed); + } + + } + + private void deleteAllLayers() { + List layers = reader.getLayers().getNames(); + for (String layerName : layers) { + RESTLayer layer = reader.getLayer(layerName); + if(layer.getType() == RESTLayer.TYPE.VECTOR) + deleteFeatureType(layer); + else if(layer.getType() == RESTLayer.TYPE.RASTER) + deleteCoverage(layer); + else + LOGGER.error("Unknown layer type " + layer.getType()); + } + + } + + private void deleteAllCoverageStores() { + List workspaces = reader.getWorkspaceNames(); + for (String workspace : workspaces) { + List stores = reader.getCoverageStores(workspace).getNames(); + + for (String storename : stores) { +// RESTCoverageStore store = reader.getCoverageStore(workspace, storename); + + LOGGER.warn("Deleting CoverageStore " + workspace + " : " + storename); + boolean removed = publisher.removeCoverageStore(workspace, storename); + assertTrue("CoverageStore not removed " + workspace + " : " + storename, removed); + } + } + } + + private void deleteAllDataStores() { + List workspaces = reader.getWorkspaceNames(); + for (String workspace : workspaces) { + List stores = reader.getDatastores(workspace).getNames(); + + for (String storename : stores) { + RESTDataStore store = reader.getDatastore(workspace, storename); + +// if(store.getType() == RESTDataStore.DBType.POSTGIS) { +// LOGGER.info("Skipping PG datastore " + store.getWorkspaceName()+":"+store.getName()); +// continue; +// } + + LOGGER.warn("Deleting DataStore " + workspace + " : " + storename); + boolean removed = publisher.removeDatastore(workspace, storename); + assertTrue("DataStore not removed " + workspace + " : " + storename, removed); + } + } + } + + private void deleteAllWorkspaces() { + List workspaces = reader.getWorkspaceNames(); + for (String workspace : workspaces) { + LOGGER.warn("Deleting Workspace " + workspace ); + boolean removed = publisher.removeWorkspace(workspace); + assertTrue("Workspace not removed " + workspace, removed ); + + } + } + + private void deleteAllStyles() { + List styles = reader.getStyles().getNames(); + for (String style : styles) { + LOGGER.warn("Deleting Style " + style ); + boolean removed = publisher.removeStyle(style); + assertTrue("Style not removed " + style, removed ); + + } + } + + private void deleteFeatureType(RESTLayer layer) { + RESTFeatureType featureType = reader.getFeatureType(layer); + RESTDataStore datastore = reader.getDatastore(featureType); + + LOGGER.warn("Deleting FeatureType" + + datastore.getWorkspaceName() + " : " + + datastore.getName() + " / " + + featureType.getName() + ); + + boolean removed = publisher.unpublishFeatureType(datastore.getWorkspaceName(), datastore.getName(), layer.getName()); + assertTrue("FeatureType not removed:" + + datastore.getWorkspaceName() + " : " + + datastore.getName() + " / " + + featureType.getName(), + removed); + + } + + private void deleteCoverage(RESTLayer layer) { + RESTCoverage coverage = reader.getCoverage(layer); + RESTCoverageStore coverageStore = reader.getCoverageStore(coverage); + + LOGGER.warn("Deleting Coverage " + + coverageStore.getWorkspaceName() + " : " + + coverageStore.getName() + " / " + + coverage.getName()); + + boolean removed = publisher.unpublishCoverage(coverageStore.getWorkspaceName(), + coverageStore.getName(), + coverage.getName()); + assertTrue("Coverage not deleted " + + coverageStore.getWorkspaceName() + " : " + + coverageStore.getName() + " / " + + coverage.getName(), + removed); + } + +} \ No newline at end of file diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties new file mode 100644 index 0000000..fd784be --- /dev/null +++ b/src/test/resources/log4j.properties @@ -0,0 +1,11 @@ + +log4j.rootLogger=DEBUG, A1 +log4j.appender.A1=org.apache.log4j.ConsoleAppender +log4j.appender.A1.layout=org.apache.log4j.PatternLayout + +# Print the date in ISO 8601 format +log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c @ %L - %m%n + +log4j.logger.it.geosolutions.geobatch.geoserver=DEBUG +log4j.logger.org.apache.commons.httpclient=INFO +log4j.logger.httpclient.wire=INFO diff --git a/src/test/resources/testdata/default_line.sld b/src/test/resources/testdata/default_line.sld new file mode 100644 index 0000000..86170d5 --- /dev/null +++ b/src/test/resources/testdata/default_line.sld @@ -0,0 +1,39 @@ + + + + + + Default Line + + + + A boring default style + A sample style that just prints out a green line + + + + + + + Rule 1 + Green Line + A green line with a 2 pixel width + + + + + #0000FF + + + + + + + + + diff --git a/src/test/resources/testdata/default_point.sld b/src/test/resources/testdata/default_point.sld new file mode 100644 index 0000000..e57a1e9 --- /dev/null +++ b/src/test/resources/testdata/default_point.sld @@ -0,0 +1,45 @@ + + + + + + Default Point + + + + A boring default style + A sample style that just prints out a purple square + + + + + + + Rule 1 + RedSquare + A red fill with an 11 pixel size + + + + + + square + + #FF0000 + + + 6 + + + + + + + + + diff --git a/src/test/resources/testdata/default_polygon.sld b/src/test/resources/testdata/default_polygon.sld new file mode 100644 index 0000000..f485002 --- /dev/null +++ b/src/test/resources/testdata/default_polygon.sld @@ -0,0 +1,43 @@ + + + + + + Default Polygon + + + + A boring default style + A sample style that just prints out a transparent red interior with a red outline + + + + + + + Rule 1 + RedFill RedOutline + 50% transparent red fill with a red outline 1 pixel in width + + + + + #AAAAAA + + + #000000 + 1 + + + + + + + + + diff --git a/src/test/resources/testdata/raster.sld b/src/test/resources/testdata/raster.sld new file mode 100644 index 0000000..77141c8 --- /dev/null +++ b/src/test/resources/testdata/raster.sld @@ -0,0 +1,20 @@ + + + + raster_layer + + raster + A boring default style + A sample style for rasters + + Feature + + + 1.0 + + + + + + \ No newline at end of file diff --git a/src/test/resources/testdata/resttestdem.tif b/src/test/resources/testdata/resttestdem.tif new file mode 100644 index 0000000000000000000000000000000000000000..49fa615897791319a8546983e545d0de20c521d0 GIT binary patch literal 102213 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 + + + country + + restteststyle + STYLE FOR TESTING PURPOSES + A sample style that just draws out a solid gray interior with a black 1px outline + + name + Feature + SemanticType[ANY] + + Polygon + + + + #C8B679 + + + 1.0 + + + + + #000000 + + + butt + + + miter + + + 1 + + + 1 + + + 0 + + + + + + CNTRYNAME + + + + Times New Roman + + + 14 + + + Normal + + + 400 + + + + + + 10 + + + + + + #000000 + + + 1.0 + + + + + + + + +