/*
* Copyright (c) 2015 Data Harmonisation Panel
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution. If not, see <http://www.gnu.org/licenses/>.
*
* Contributors:
* Data Harmonisation Panel <http://www.dhpanel.eu>
*/
package eu.esdihumboldt.hale.io.geoserver.rest;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.fluent.Executor;
import org.apache.http.client.fluent.Request;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import com.google.common.base.Joiner;
import eu.esdihumboldt.hale.io.geoserver.Resource;
/**
* Base class for classes representing GeoServer resource managers.
*
* <p>
* The basic idea is that a resource manager can retrieve the list of resources
* of type <code>T</code> and can execute the standard CRUD operations on a
* specific resource instance, called the "managed resource", which must be
* explicitly set by calling the {@link #setResource(Resource)} method.
* </p>
*
* @author Stefano Costa, GeoSolutions
* @param <T> the type of the managed resource
*/
public abstract class AbstractResourceManager<T extends Resource> implements ResourceManager<T> {
/**
* Base path for all REST services.
*/
public static final String REST_BASE = "rest";
/**
* Default request / response body format.
*/
public static final String DEF_FORMAT = "xml";
/**
* Base GeoServer URL (e.g. http://localhost:8080/geoserver)
*/
protected URL geoserverUrl;
/**
* The resource to manage.
*/
protected T resource;
private final Executor executor;
/**
* Constructor.
*
* @param geoserverUrl the base GeoServer URL
* @throws MalformedURLException if the provided URL is invalid
*/
public AbstractResourceManager(String geoserverUrl) throws MalformedURLException {
this(new URL(geoserverUrl));
}
/**
* Constructor.
*
* @param geoserverUrl the base GeoServer URL
*/
public AbstractResourceManager(URL geoserverUrl) {
if (geoserverUrl == null || geoserverUrl.getQuery() != null) {
throw new IllegalArgumentException(
"GeoServer base URL must not be null and must not contain a query part");
}
this.geoserverUrl = geoserverUrl;
this.executor = Executor.newInstance();
}
/**
* @see eu.esdihumboldt.hale.io.geoserver.rest.ResourceManager#setCredentials(java.lang.String,
* java.lang.String)
*/
@Override
public void setCredentials(String user, String password) {
HttpHost geoserverHost = new HttpHost(geoserverUrl.getHost(), geoserverUrl.getPort(),
geoserverUrl.getProtocol());
executor.auth(geoserverHost, user, password);
}
/**
* @see eu.esdihumboldt.hale.io.geoserver.rest.ResourceManager#list()
*/
@Override
public Document list() {
try {
return executor.execute(Request.Get(getResourceListURL()))
.handleResponse(new XmlResponseHandler());
} catch (Exception e) {
throw new ResourceException(e);
}
}
/**
* @see eu.esdihumboldt.hale.io.geoserver.rest.ResourceManager#setResource(eu.esdihumboldt.hale.io.geoserver.Resource)
*/
@Override
public void setResource(T resource) {
this.resource = resource;
}
/**
* @see eu.esdihumboldt.hale.io.geoserver.rest.ResourceManager#exists()
*/
@Override
public boolean exists() {
checkResourceSet();
try {
return executor.execute(Request.Get(getResourceURL()))
.handleResponse(new ResponseHandler<Boolean>() {
/**
* @see org.apache.http.client.ResponseHandler#handleResponse(org.apache.http.HttpResponse)
*/
@Override
public Boolean handleResponse(HttpResponse response)
throws ClientProtocolException, IOException {
int statusCode = response.getStatusLine().getStatusCode();
String reason = response.getStatusLine().getReasonPhrase();
switch (statusCode) {
case 200:
return true;
case 404:
return false;
default:
throw new HttpResponseException(statusCode, reason);
}
}
});
} catch (Exception e) {
throw new ResourceException(e);
}
}
/**
* @see eu.esdihumboldt.hale.io.geoserver.rest.ResourceManager#read()
*/
@Override
public Document read() {
return read(null);
}
/**
* @see eu.esdihumboldt.hale.io.geoserver.rest.ResourceManager#read(java.util.Map)
*/
@Override
public Document read(Map<String, String> parameters) {
checkResourceSet();
try {
URI requestUri = buildRequestUri(getResourceURL(), parameters);
return executor.execute(Request.Get(requestUri))
.handleResponse(new XmlResponseHandler());
} catch (Exception e) {
throw new ResourceException(e);
}
}
/**
* @see eu.esdihumboldt.hale.io.geoserver.rest.ResourceManager#create()
*/
@Override
public URL create() {
return create(null);
}
/**
* @see eu.esdihumboldt.hale.io.geoserver.rest.ResourceManager#create(java.util.Map)
*/
@Override
public URL create(Map<String, String> parameters) {
checkResourceSet();
try {
URI requestUri = buildRequestUri(getResourceListURL(), parameters);
ByteArrayEntity entity = new ByteArrayEntity(resource.asByteArray());
entity.setContentType(resource.contentType().getMimeType());
return executor.execute(Request.Post(requestUri).body(entity))
.handleResponse(new ResponseHandler<URL>() {
/**
* @see org.apache.http.client.ResponseHandler#handleResponse(org.apache.http.HttpResponse)
*/
@Override
public URL handleResponse(HttpResponse response)
throws ClientProtocolException, IOException {
StatusLine statusLine = response.getStatusLine();
if (statusLine.getStatusCode() >= 300) {
throw new HttpResponseException(statusLine.getStatusCode(),
statusLine.getReasonPhrase());
}
if (statusLine.getStatusCode() == 201) {
Header locationHeader = response.getFirstHeader("Location");
if (locationHeader != null) {
return new URL(locationHeader.getValue());
}
}
return null;
}
});
} catch (Exception e) {
throw new ResourceException(e);
}
}
/**
* @see eu.esdihumboldt.hale.io.geoserver.rest.ResourceManager#update()
*/
@Override
public void update() {
update(null);
}
/**
* @see eu.esdihumboldt.hale.io.geoserver.rest.ResourceManager#update(java.util.Map)
*/
@Override
public void update(Map<String, String> parameters) {
checkResourceSet();
try {
URI requestUri = buildRequestUri(getResourceURL(), parameters);
ByteArrayEntity entity = new ByteArrayEntity(resource.asByteArray());
entity.setContentType(resource.contentType().getMimeType());
executor.execute(Request.Put(requestUri).body(entity))
.handleResponse(new EmptyResponseHandler());
} catch (Exception e) {
throw new ResourceException(e);
}
}
/**
* @see eu.esdihumboldt.hale.io.geoserver.rest.ResourceManager#delete()
*/
@Override
public void delete() {
delete(null);
}
/**
* @see eu.esdihumboldt.hale.io.geoserver.rest.ResourceManager#delete(java.util.Map)
*/
@Override
public void delete(Map<String, String> parameters) {
checkResourceSet();
try {
URI requestUri = buildRequestUri(getResourceURL(), parameters);
executor.execute(Request.Delete(requestUri)).handleResponse(new EmptyResponseHandler());
} catch (Exception e) {
throw new ResourceException(e);
}
}
private void checkResourceSet() {
if (this.resource == null) {
throw new IllegalStateException("Resource not set");
}
}
private URI buildRequestUri(String url, Map<String, String> parameters)
throws URISyntaxException {
URIBuilder uriBuilder = new URIBuilder(url);
if (parameters != null) {
parameters.forEach((param, value) -> uriBuilder.addParameter(param, value));
}
return uriBuilder.build();
}
/**
* Construct URL of the list of resources of type <code>T</code>.
*
* @return the resource list URL
*/
protected String getResourceListURL() {
return getRestServiceUrl(getResourceListPath());
}
/**
* Construct URL of the managed resource.
*
* @return the resource URL
*/
protected String getResourceURL() {
return getRestServiceUrl(getResourcePath());
}
private String getRestServiceUrl(String resourcePath) {
List<String> urlParts = new ArrayList<String>();
urlParts.add(geoserverUrl.toString());
urlParts.add(REST_BASE);
urlParts.add(resourcePath);
urlParts.replaceAll(urlPart -> normalizeUrlPart(urlPart));
final String resourceUrl = Joiner.on('/').skipNulls().join(urlParts);
return Joiner.on(".").skipNulls().join(Arrays.asList(resourceUrl, getFormat()));
}
private String normalizeUrlPart(String urlPart) {
// remove slashes at the beginning and end of the URL part
// and return null if it empty or contains only whitespace
urlPart = StringUtils.stripStart(urlPart, "/");
urlPart = StringUtils.stripEnd(urlPart, "/");
return StringUtils.defaultIfBlank(urlPart, null);
}
/**
* Template method, to be implemented by subclasses.
*
* <p>
* Should return the path to the list of resources of type <code>T</code>
* (relative to GeoServer's REST base path, e.g.
* http://localhost:8080/geoserver/rest).
*
* @return the resource list path
*/
protected abstract String getResourceListPath();
/**
* Template method, to be implemented by subclasses.
*
* <p>
* Should return the path to the managed resource (relative to GeoServer's
* REST base path, e.g. http://localhost:8080/geoserver/rest).
* </p>
*
* @return the resource path
*/
protected abstract String getResourcePath();
/**
* Retrieves the default request / response body format.
*
* @return the format
*/
protected String getFormat() {
return DEF_FORMAT;
}
/**
* Response handler that parses the response body into an XML
* {@link Document}.
*
* @author Stefano Costa, GeoSolutions
*/
private class XmlResponseHandler implements ResponseHandler<Document> {
/**
* @see org.apache.http.client.ResponseHandler#handleResponse(org.apache.http.HttpResponse)
*/
@Override
public Document handleResponse(HttpResponse response)
throws ClientProtocolException, IOException {
StatusLine statusLine = response.getStatusLine();
HttpEntity entity = response.getEntity();
if (statusLine.getStatusCode() >= 300) {
throw new HttpResponseException(statusLine.getStatusCode(),
statusLine.getReasonPhrase());
}
if (entity == null) {
throw new ClientProtocolException("Response contains no content");
}
DocumentBuilderFactory dbFac = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder docBuilder = dbFac.newDocumentBuilder();
ContentType contentType = ContentType.getOrDefault(entity);
if (!isXml(contentType)) {
throw new ClientProtocolException("Unexpected content type: " + contentType);
}
Charset charset = contentType.getCharset();
if (charset == null) {
charset = Charset.forName("UTF-8");
}
return docBuilder.parse(entity.getContent());
} catch (ParserConfigurationException ex) {
throw new IllegalStateException(ex);
} catch (SAXException ex) {
throw new ClientProtocolException("Malformed XML document", ex);
}
}
}
private boolean isXml(ContentType contentType) {
if (contentType == null) {
return false;
}
String mimeType = contentType.getMimeType();
return mimeType.equals("text/xml") || mimeType.equals("application/xml");
}
/**
* Response handler that does nothing with the response body, but just
* checks the response status and throws an exception if the status code is
* greater than or equal to 300.
*
* @author Stefano Costa, GeoSolutions
*/
private class EmptyResponseHandler implements ResponseHandler<Void> {
/**
* @see org.apache.http.client.ResponseHandler#handleResponse(org.apache.http.HttpResponse)
*/
@Override
public Void handleResponse(HttpResponse response)
throws ClientProtocolException, IOException {
StatusLine statusLine = response.getStatusLine();
if (statusLine.getStatusCode() >= 300) {
throw new HttpResponseException(statusLine.getStatusCode(),
statusLine.getReasonPhrase());
}
return null;
}
}
}