/**
* This program is free software: you can redistribute it and/or modify
* it 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.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Marius Suta / The Open Planning Project 2008
* @author Arne Kepp / The Open Planning Project 2009
*/
package org.geowebcache.rest.layers;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.geowebcache.GeoWebCacheDispatcher;
import org.geowebcache.GeoWebCacheException;
import org.geowebcache.config.Configuration;
import org.geowebcache.config.ContextualConfigurationProvider.Context;
import org.geowebcache.config.XMLConfiguration;
import org.geowebcache.filter.parameters.ParameterFilter;
import org.geowebcache.io.GeoWebCacheXStream;
import org.geowebcache.layer.TileLayer;
import org.geowebcache.layer.TileLayerDispatcher;
import org.geowebcache.rest.GWCRestlet;
import org.geowebcache.rest.RestletException;
import org.geowebcache.rest.XstreamRepresentation;
import org.geowebcache.service.HttpErrorCodeException;
import org.geowebcache.storage.StorageBroker;
import org.geowebcache.storage.StorageException;
import org.geowebcache.util.NullURLMangler;
import org.geowebcache.util.ServletUtils;
import org.geowebcache.util.URLMangler;
import org.json.JSONException;
import org.json.JSONObject;
import org.restlet.data.CharacterSet;
import org.restlet.data.MediaType;
import org.restlet.data.Method;
import org.restlet.data.Request;
import org.restlet.data.Response;
import org.restlet.data.Status;
import org.restlet.ext.json.JsonRepresentation;
import org.restlet.resource.Representation;
import org.restlet.resource.StringRepresentation;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.ConversionException;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamDriver;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.copy.HierarchicalStreamCopier;
import com.thoughtworks.xstream.io.json.JettisonMappedXmlDriver;
import com.thoughtworks.xstream.io.json.JsonHierarchicalStreamDriver;
import com.thoughtworks.xstream.io.xml.DomDriver;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
/**
* This is the TileLayer resource class required by the REST interface
*/
public class TileLayerRestlet extends GWCRestlet {
private XMLConfiguration xmlConfig;
private TileLayerDispatcher layerDispatcher;
private URLMangler urlMangler = new NullURLMangler();
private GeoWebCacheDispatcher controller = null;
private StorageBroker storageBroker;
// set by spring
public void setUrlMangler(URLMangler urlMangler) {
this.urlMangler = urlMangler;
}
// set by spring
public void setController(GeoWebCacheDispatcher controller) {
this.controller = controller;
}
// set by spring
public void setStorageBroker(StorageBroker storageBroker) {
this.storageBroker = storageBroker;
}
@Override
public void handle(Request request, Response response) {
Method met = request.getMethod();
try {
if (met.equals(Method.GET)) {
doGet(request, response);
} else {
if(Objects.isNull(request.getAttributes().get("layer"))) {
throw new RestletException("Method not allowed",
Status.CLIENT_ERROR_METHOD_NOT_ALLOWED);
}
// These modify layers, so we reload afterwards
if (met.equals(Method.POST)) {
doPost(request, response);
} else if (met.equals(Method.PUT)) {
doPut(request, response);
} else if (met.equals(Method.DELETE)) {
doDelete(request, response);
} else {
throw new RestletException("Method not allowed",
Status.CLIENT_ERROR_METHOD_NOT_ALLOWED);
}
}
} catch (RestletException re) {
response.setEntity(re.getRepresentation());
response.setStatus(re.getStatus());
} catch (HttpErrorCodeException httpException) {
int errorCode = httpException.getErrorCode();
Status status = Status.valueOf(errorCode);
response.setStatus(status);
response.setEntity(httpException.getMessage(), MediaType.TEXT_PLAIN);
} catch (Exception e) {
// Either GeoWebCacheException or IOException
response.setEntity(e.getMessage() + " " + e.toString(), MediaType.TEXT_PLAIN);
response.setStatus(Status.SERVER_ERROR_INTERNAL);
e.printStackTrace();
}
}
/**
* GET outputs an existing layer
*
* @param req
* @param resp
* @throws RestletException
* @throws
*/
protected void doGet(Request req, Response resp) throws RestletException {
String layerName = (String) req.getAttributes().get("layer");
final String formatExtension = (String) req.getAttributes().get("extension");
Representation representation;
if (layerName == null) {
String restRoot = req.getRootRef().toString();
String contextPath = req.getRootRef().getPath();
String servletPrefix = null;
if (controller!=null) servletPrefix=controller.getServletPrefix();
String parentPath="";
int spIndex = 0;
if(servletPrefix!=null){
spIndex = contextPath.indexOf(servletPrefix);
parentPath = contextPath.substring(0, spIndex);
}
contextPath = contextPath.substring(spIndex, contextPath.length());
String baseURL = restRoot.substring(0, restRoot.length() - contextPath.length());
String prefix = req.getResourceRef().getParentRef().getPath();
prefix = prefix.substring(parentPath.length(), prefix.length());
representation = listLayers(formatExtension, baseURL, prefix);
} else {
try {
layerName = URLDecoder.decode(layerName, "UTF-8");
} catch (UnsupportedEncodingException uee) {
}
representation = doGetInternal(layerName, formatExtension);
}
resp.setEntity(representation);
}
/**
* @param extension
* @param rootPath
* @param contextPath
* @return
*/
Representation listLayers(String extension, final String rootPath, final String contextPath) {
if (null == extension) {
extension = "xml";
}
List<String> layerNames = new ArrayList<String>(layerDispatcher.getLayerNames());
Collections.sort(layerNames);
Representation representation;
if (extension.equalsIgnoreCase("xml")) {
representation = new XstreamRepresentation(layerNames);
representation.setCharacterSet(CharacterSet.UTF_8);
final XStream xStream = ((XstreamRepresentation) representation).getXStream();
xStream.alias("layers", List.class);
xmlConfig.getConfiguredXStreamWithContext(xStream, Context.REST);
xStream.registerConverter(new Converter() {
/**
* @see com.thoughtworks.xstream.converters.ConverterMatcher#canConvert(java.lang.Class)
*/
public boolean canConvert(@SuppressWarnings("rawtypes") Class type) {
return List.class.isAssignableFrom(type);
}
/**
* @see com.thoughtworks.xstream.converters.Converter#marshal(java.lang.Object,
* com.thoughtworks.xstream.io.HierarchicalStreamWriter,
* com.thoughtworks.xstream.converters.MarshallingContext)
*/
public void marshal(Object source, HierarchicalStreamWriter writer,
MarshallingContext context) {
@SuppressWarnings("unchecked")
List<String> layers = (List<String>) source;
for (String name : layers) {
writer.startNode("layer");
writer.startNode("name");
writer.setValue(name);
writer.endNode(); // name
writer.startNode("atom:link");
writer.addAttribute("xmlns:atom", "http://www.w3.org/2005/Atom");
writer.addAttribute("rel", "alternate");
String href = urlMangler.buildURL(rootPath, contextPath, "/layers/"
+ ServletUtils.URLEncode(name) + ".xml");
writer.addAttribute("href", href);
writer.addAttribute("type", MediaType.TEXT_XML.toString());
writer.endNode();
writer.endNode();// layer
}
}
/**
* @see com.thoughtworks.xstream.converters.Converter#unmarshal(com.thoughtworks.xstream.io.HierarchicalStreamReader,
* com.thoughtworks.xstream.converters.UnmarshallingContext)
*/
public Object unmarshal(HierarchicalStreamReader reader,
UnmarshallingContext context) {
throw new UnsupportedOperationException();
}
});
} else if (extension.equalsIgnoreCase("html")) {
throw new RestletException("Unknown or missing format extension : " + extension,
Status.CLIENT_ERROR_BAD_REQUEST);
} else {
throw new RestletException("Unknown or missing format extension : " + extension,
Status.CLIENT_ERROR_BAD_REQUEST);
}
return representation;
}
/**
* We separate out the internal to make unit testing easier
*
* @param layerName
* @param formatExtension
* @return
* @throws RestletException
*/
protected Representation doGetInternal(String layerName, String formatExtension)
throws RestletException {
TileLayer tl = findTileLayer(layerName, layerDispatcher);
if (formatExtension.equalsIgnoreCase("xml")) {
return getXMLRepresentation(tl);
} else if (formatExtension.equalsIgnoreCase("json")) {
return getJsonRepresentation(tl);
} else {
throw new RestletException("Unknown or missing format extension : " + formatExtension,
Status.CLIENT_ERROR_BAD_REQUEST);
}
}
/**
* POST overwrites an existing layer
*
* @param req
* @param resp
* @throws RestletException
*/
private void doPost(Request req, Response resp) throws RestletException, IOException,
GeoWebCacheException {
TileLayer tl = deserializeAndCheckLayer(req, resp, false);
try {
Configuration configuration = layerDispatcher.modify(tl);
configuration.save();
} catch (IllegalArgumentException e) {
throw new RestletException("Layer " + tl.getName()
+ " is not known by the configuration."
+ "Maybe it was loaded from another source, or you're trying to add a new "
+ "layer and need to do an HTTP PUT ?", Status.CLIENT_ERROR_BAD_REQUEST);
}
}
/**
* PUT creates a new layer
*
* @param req
* @param resp
* @throws RestletException
*/
private void doPut(Request req, Response resp) throws RestletException, IOException,
GeoWebCacheException {
TileLayer tl = deserializeAndCheckLayer(req, resp, true);
TileLayer testtl = null;
try {
testtl = findTileLayer(tl.getName(), layerDispatcher);
} catch (RestletException re) {
// This is the expected behavior, it should not exist
}
if (testtl == null) {
Configuration config = layerDispatcher.addLayer(tl);
config.save();
} else {
throw new RestletException("Layer with name " + tl.getName() + " already exists, "
+ "use POST if you want to replace it.", Status.CLIENT_ERROR_BAD_REQUEST);
}
}
/**
* DELETE removes an existing layer
*
* @param req
* @param resp
* @throws RestletException
*/
private void doDelete(Request req, Response resp) throws RestletException, GeoWebCacheException {
String layerName = ServletUtils.URLDecode((String) req.getAttributes().get("layer"),
"UTF-8");
findTileLayer(layerName, layerDispatcher);
// TODO: refactor storage management to use a comprehensive event system;
// centralise duplicate functionality from GeoServer gs-gwc GWC.layerRemoved
// and CatalogConfiguration.removeLayer into GeoWebCache and use event system
// to ensure removal and rename operations are atomic and consistent. Until this
// is done, the following is a temporary workaround:
//
// delete cached tiles first in case a blob store
// uses the configuration to perform the deletion
StorageException storageBrokerDeleteException = null;
try {
storageBroker.delete(layerName);
} catch (StorageException se) {
// save exception for later so failure to delete
// cached tiles does not prevent layer removal
storageBrokerDeleteException = se;
}
try {
Configuration configuration = layerDispatcher.removeLayer(layerName);
if (configuration == null) {
throw new RestletException("Configuration to remove layer not found",
Status.SERVER_ERROR_INTERNAL);
}
configuration.save();
} catch (IOException e) {
throw new RestletException(e.getMessage(), Status.SERVER_ERROR_INTERNAL, e);
}
if (storageBrokerDeleteException != null) {
// layer removal worked, so report failure to delete cached tiles
throw new RestletException(
"Removal of layer " + layerName
+ " was successful but deletion of cached tiles failed: "
+ storageBrokerDeleteException.getMessage(),
Status.SERVER_ERROR_INTERNAL, storageBrokerDeleteException);
}
}
protected TileLayer deserializeAndCheckLayer(Request req, Response resp, boolean isPut)
throws RestletException, IOException {
// TODO UTF-8 may not always be right here
String layerName = ServletUtils.URLDecode((String) req.getAttributes().get("layer"),
"UTF-8");
String formatExtension = (String) req.getAttributes().get("extension");
InputStream is = req.getEntity().getStream();
// If appropriate, check whether this layer exists
if (!isPut) {
findTileLayer(layerName, layerDispatcher);
}
return deserializeAndCheckLayerInternal(layerName, formatExtension, is);
}
/**
* We separate out the internal to make unit testing easier
*
* @param layerName
* @param formatExtension
* @param is
* @return
* @throws RestletException
* @throws IOException
*/
protected TileLayer deserializeAndCheckLayerInternal(String layerName, String formatExtension,
InputStream is) throws RestletException, IOException {
XStream xs = xmlConfig.getConfiguredXStreamWithContext(new GeoWebCacheXStream(new DomDriver()), Context.REST);
TileLayer newLayer;
try {
if (formatExtension.equalsIgnoreCase("xml")) {
newLayer = (TileLayer) xs.fromXML(is);
} else if (formatExtension.equalsIgnoreCase("json")) {
HierarchicalStreamDriver driver = new JettisonMappedXmlDriver();
HierarchicalStreamReader hsr = driver.createReader(is);
// See http://jira.codehaus.org/browse/JETTISON-48
StringWriter writer = new StringWriter();
new HierarchicalStreamCopier().copy(hsr, new PrettyPrintWriter(writer));
writer.close();
newLayer = (TileLayer) xs.fromXML(writer.toString());
} else {
throw new RestletException("Unknown or missing format extension: "
+ formatExtension, Status.CLIENT_ERROR_BAD_REQUEST);
}
} catch (ConversionException xstreamExceptionWrapper) {
Throwable cause = xstreamExceptionWrapper.getCause();
if (cause instanceof Error) {
throw (Error) cause;
}
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause!=null){
throw new RestletException(cause.getMessage(), Status.SERVER_ERROR_INTERNAL,
(Exception) cause);
} else {
throw new RestletException(xstreamExceptionWrapper.getMessage(),
Status.SERVER_ERROR_INTERNAL, xstreamExceptionWrapper);
}
}
if (!newLayer.getName().equals(layerName)) {
throw new RestletException("There is a mismatch between the name of the "
+ " layer in the submission and the URL you specified.",
Status.CLIENT_ERROR_BAD_REQUEST);
}
// Check that the parameter filters deserialized correctly
if(newLayer.getParameterFilters()!=null) {
try {
for(@SuppressWarnings("unused")
ParameterFilter filter: newLayer.getParameterFilters()){
// Don't actually need to do anything here. Just iterate over the elements
// casting them into ParameterFilter
}
} catch (ClassCastException ex) {
// By this point it has already been turned into a POJO, so the XML is no longer
// available. Otherwise it would be helpful to include in the error message.
throw new RestletException("parameterFilters contains an element that is not "+
"a known ParameterFilter", Status.CLIENT_ERROR_BAD_REQUEST);
}
}
return newLayer;
}
/**
* Returns a XMLRepresentation of the layer
*
* @param layer
* @return
*/
public Representation getXMLRepresentation(TileLayer layer) {
XStream xs = xmlConfig.getConfiguredXStreamWithContext(new GeoWebCacheXStream(), Context.REST);
String xmlText = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xs.toXML(layer);
return new StringRepresentation(xmlText, MediaType.TEXT_XML);
}
/**
* Returns a JsonRepresentation of the layer
*
* @param layer
* @return
*/
public JsonRepresentation getJsonRepresentation(TileLayer layer) {
JsonRepresentation rep = null;
try {
XStream xs = xmlConfig.getConfiguredXStreamWithContext(new GeoWebCacheXStream(
new JsonHierarchicalStreamDriver()), Context.REST);
JSONObject obj = new JSONObject(xs.toXML(layer));
rep = new JsonRepresentation(obj);
} catch (JSONException jse) {
jse.printStackTrace();
}
return rep;
}
public void setTileLayerDispatcher(TileLayerDispatcher tileLayerDispatcher) {
layerDispatcher = tileLayerDispatcher;
}
public void setXMLConfiguration(XMLConfiguration xmlConfig) {
this.xmlConfig = xmlConfig;
}
}