/* (c) 2014 Boundless, http://boundlessgeo.com
* This code is licensed under the GPL 2.0 license.
*/
package com.boundlessgeo.geoserver.api.controllers;
import static org.geoserver.catalog.Predicates.and;
import static org.geoserver.catalog.Predicates.equal;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang.WordUtils;
import org.geoserver.catalog.CascadeDeleteVisitor;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CatalogFactory;
import org.geoserver.catalog.CoverageInfo;
import org.geoserver.catalog.CoverageStoreInfo;
import org.geoserver.catalog.DataStoreInfo;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.catalog.ResourcePool;
import org.geoserver.catalog.StoreInfo;
import org.geoserver.catalog.WMSStoreInfo;
import org.geoserver.catalog.WorkspaceInfo;
import org.geoserver.catalog.util.CloseableIterator;
import org.geoserver.config.GeoServer;
import org.geoserver.ows.URLMangler.URLType;
import org.geoserver.platform.GeoServerResourceLoader;
import org.geoserver.platform.resource.Files;
import org.geoserver.platform.resource.Paths;
import org.geotools.coverage.grid.io.AbstractGridFormat;
import org.geotools.coverage.grid.io.GridCoverage2DReader;
import org.geotools.data.DataAccess;
import org.geotools.data.DataAccessFinder;
import org.geotools.data.DataStore;
import org.geotools.data.ows.Layer;
import org.geotools.data.wms.WebMapServer;
import org.geotools.feature.NameImpl;
import org.geotools.util.Converters;
import org.geotools.util.NullProgressListener;
import org.geotools.util.logging.Logging;
import org.opengis.coverage.grid.Format;
import org.opengis.coverage.grid.GridCoverageReader;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.Name;
import org.opengis.filter.Filter;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.boundlessgeo.geoserver.json.JSONArr;
import com.boundlessgeo.geoserver.json.JSONObj;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
/**
* Used to connect to data storage (file, database, or service).
* <p>
* This API is locked down for map composer and is (not intended to be stable between releases).</p>
*
* @see <a href="https://github.com/boundlessgeo/suite/wiki/Stores-API">Store API</a> (Wiki)
*/
@Controller
@RequestMapping("/api/stores")
public class StoreController extends ApiController {
static Logger LOG = Logging.getLogger(StoreController.class);
@Autowired
public StoreController(GeoServer geoServer) {
super(geoServer);
}
@RequestMapping(value = "/{wsName}", method = RequestMethod.GET)
public @ResponseBody
JSONArr list(@PathVariable String wsName){
JSONArr arr = new JSONArr();
Catalog cat = geoServer.getCatalog();
for (StoreInfo store : cat.getStoresByWorkspace(wsName, StoreInfo.class)) {
store(arr.addObject(), store);
}
return arr;
}
@RequestMapping(value = "/{wsName}/{name}", method = RequestMethod.GET)
public @ResponseBody
JSONObj get(@PathVariable String wsName, @PathVariable String name, HttpServletRequest req) {
StoreInfo store = findStore(wsName, name, geoServer.getCatalog());
if (store == null) {
throw new IllegalArgumentException("Store " + wsName + ":" + name + " not found");
}
try {
return storeDetails(new JSONObj(), store,req);
} catch (IOException e) {
throw new RuntimeException(String.format("Error occured accessing store: %s,%s",wsName, name), e);
}
}
@RequestMapping(value = "/{wsName}/{stName}/{name}", method = RequestMethod.GET)
public @ResponseBody JSONObj resource(@PathVariable String wsName, @PathVariable String stName, @PathVariable String name, HttpServletRequest req) throws IOException {
Catalog cat = geoServer.getCatalog();
StoreInfo store = findStore(wsName, stName, cat );
JSONObj obj = resource( new JSONObj(), store, name, req);
obj.putObject("store")
.put("name", stName )
.put("url", IO.url(req, "/stores/%s/%s",wsName,stName));
return obj;
}
@RequestMapping(value = "/{wsName}/{name}", method = RequestMethod.DELETE)
public @ResponseBody
JSONObj delete(@PathVariable String wsName,
@PathVariable String name,
@RequestParam(value="recurse",defaultValue="false") boolean recurse,
HttpServletRequest req) {
StoreInfo store = findStore(wsName, name, geoServer.getCatalog());
Catalog cat = geoServer.getCatalog();
List<ResourceInfo> layers = cat.getResourcesByStore(store, ResourceInfo.class );
if( layers.isEmpty() ){
cat.remove(store);
}
else if (recurse){
CascadeDeleteVisitor delete = new CascadeDeleteVisitor(cat);
if( store instanceof DataStoreInfo){
delete.visit((DataStoreInfo)store);
}
else if( store instanceof CoverageStoreInfo){
delete.visit((CoverageStoreInfo)store);
}
else if( store instanceof WMSStoreInfo){
delete.visit((WMSStoreInfo)store);
}
else {
throw new IllegalStateException( "Unable to delete "+name+" - expected data store, coverage store or wms store" );
}
}
else {
StringBuilder message = new StringBuilder();
message.append("Use recurse=true to remove ").append(name).append(" along with layers:");
for( ResourceInfo l : layers ){
message.append(' ').append(l.getName());
}
throw new IllegalStateException( message.toString() );
}
JSONObj json = new JSONObj();
json.put("name", name )
.put("workspace", wsName );
return json;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@RequestMapping(value = "/{wsName}/{name}", method = RequestMethod.POST)
public @ResponseBody
JSONObj create(@PathVariable String wsName, @PathVariable String name, @RequestBody JSONObj obj, HttpServletRequest req) throws IOException {
Catalog cat = geoServer.getCatalog();
CatalogFactory factory = cat.getFactory();
WorkspaceInfo workspace = findWorkspace(wsName);
StoreInfo store = null;
JSONObj params = obj.object("connection");
if( params == null ){
throw new IllegalArgumentException("connection parameters required");
}
if( params.has("raster")){
String url = params.str("raster");
CoverageStoreInfo info = factory.createCoverageStore();
info.setWorkspace(workspace);
info.setType(name);
// connect and defaults
info.setURL(url);
info.setType(obj.str("type"));
try {
GridCoverageReader reader = info.getGridCoverageReader(null, null);
Format format = reader.getFormat();
info.setDescription( format.getDescription() );
info.setEnabled(true);
} catch (IOException e) {
info.setError(e);
info.setEnabled(false);
}
store = info;
}
else if ( params.has("url") &&
params.str("url").toLowerCase().contains("Service=WMS") &&
params.str("url").startsWith("http")){
WMSStoreInfo info = factory.createWebMapServer();
info.setWorkspace(workspace);
info.setType(name);
// connect and defaults
info.setCapabilitiesURL(params.str("url"));
try {
WebMapServer service = info.getWebMapServer(new NullProgressListener());
info.setDescription( service.getInfo().getDescription() );
info.setEnabled(true);
} catch (Throwable e) {
info.setError(e);
info.setEnabled(false);
}
store = info;
}
else {
HashMap map = new HashMap(params.raw());
Map resolved = ResourcePool.getParams(map, cat.getResourceLoader() );
DataAccess dataStore = DataAccessFinder.getDataStore(resolved);
if( dataStore == null ){
throw new IllegalArgumentException("Connection parameters incomplete (does not match an available data store, coverage store or wms store).");
}
DataStoreInfo info = factory.createDataStore();
info.setWorkspace(workspace);
info.setType(name);
info.getConnectionParameters().putAll(map);
try {
info.setDescription( dataStore.getInfo().getDescription());
info.setEnabled(true);
} catch (Throwable e) {
info.setError(e);
info.setEnabled(false);
}
store = info;
}
boolean refresh = define( store, obj );
if( refresh ){
LOG.log( Level.FINE, "Inconsistent: default connection used for store creation required refresh");
}
cat.add(store);
return storeDetails(new JSONObj(), store,req);
}
@RequestMapping(value="/{wsName}/{name}", method = RequestMethod.PATCH)
public @ResponseBody JSONObj patch(@PathVariable String wsName, @PathVariable String name, @RequestBody JSONObj obj, HttpServletRequest req) throws IOException {
Catalog cat = geoServer.getCatalog();
StoreInfo store = cat.getStoreByName(wsName, name, StoreInfo.class );
boolean refresh = define(store, obj);
cat.save(store);
if (refresh) {
resetConnection(store);
}
return storeDetails(new JSONObj(), store,req);
}
private void resetConnection(StoreInfo store ){
Catalog cat = geoServer.getCatalog();
if (store instanceof CoverageStoreInfo) {
cat.getResourcePool().clear((CoverageStoreInfo) store);
} else if (store instanceof DataStoreInfo) {
cat.getResourcePool().clear((DataStoreInfo) store);
} else if (store instanceof WMSStoreInfo) {
cat.getResourcePool().clear((WMSStoreInfo) store);
}
}
@RequestMapping(value="/{wsName}/{name}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE)
public @ResponseBody JSONObj put(@PathVariable String wsName, @PathVariable String name, @RequestBody JSONObj obj, HttpServletRequest req) throws IOException {
Catalog cat = geoServer.getCatalog();
StoreInfo store = cat.getStoreByName(wsName, name, StoreInfo.class );
// pending: clear store to defaults
boolean refresh = define( store, obj );
cat.save( store );
if (refresh) {
resetConnection(store);
}
return storeDetails(new JSONObj(), store,req);
}
@SuppressWarnings("unchecked")
private boolean define( StoreInfo store, JSONObj obj ){
boolean reconnect = false;
for( String prop : obj.keys()){
if("description".equals(prop)){
store.setDescription(obj.str(prop));
}
else if("enabled".equals(prop)){
store.setEnabled(obj.bool(prop));
reconnect = true;
}
else if("name".equals(prop)){
store.setName(obj.str(prop));
}
else if("workspace".equals(prop)){
WorkspaceInfo newWorkspace = findWorkspace(obj.str(prop));
store.setWorkspace( newWorkspace );
}
else if( store instanceof CoverageStoreInfo){
CoverageStoreInfo info = (CoverageStoreInfo) store;
if("connection".equals(prop)){
JSONObj connection = obj.object(prop);
if(!connection.has("raster") && connection.str("raster") != null){
throw new IllegalArgumentException("Property connection.raster required for coverage store");
}
for( String param : connection.keys()){
if("raster".equals(param)){
String url = connection.str(param);
reconnect = reconnect || url == null || !url.equals(info.getURL());
info.setURL(url);
}
}
}
}
else if( store instanceof WMSStoreInfo){
WMSStoreInfo info = (WMSStoreInfo) store;
if("connection".equals(prop)){
JSONObj connection = obj.object(prop);
if(!connection.has("url") && connection.str("url") != null){
throw new IllegalArgumentException("Property connection.url required for wms store");
}
for( String param : connection.keys()){
if("url".equals(param)){
String url = connection.str(param);
reconnect = reconnect || url == null || !url.equals(info.getCapabilitiesURL());
info.setCapabilitiesURL(url);
}
}
}
}
if( store instanceof DataStoreInfo){
DataStoreInfo info = (DataStoreInfo) store;
if("connection".equals(prop)){
JSONObj connection = obj.object(prop);
info.getConnectionParameters().clear();
info.getConnectionParameters().putAll( connection.raw() );
reconnect = true;
}
}
}
return reconnect;
}
private JSONObj store(JSONObj obj, StoreInfo store) {
String name = store.getName();
obj.put("name", name)
.put("workspace", store.getWorkspace().getName())
.put("enabled", store.isEnabled())
.put("description", store.getDescription())
.put("format", store.getType());
String source = source(store);
obj.put("source", source )
.put("type", IO.Kind.of(store).name())
.put("kind", IO.Type.of(store).name());
return IO.metadata(obj, store);
}
private JSONObj storeDetails(JSONObj json, StoreInfo store, HttpServletRequest req) throws IOException {
store(json, store);
JSONObj connection = new JSONObj();
Map<String, Serializable> params = store.getConnectionParameters();
for (Entry<String, Serializable> param : params.entrySet()) {
String key = param.getKey();
Object value = param.getValue();
String text = value == null ? null : value.toString();
connection.put( key, text );
}
if (store instanceof CoverageStoreInfo) {
CoverageStoreInfo info = (CoverageStoreInfo) store;
connection.put("raster", info.getURL());
}
if (store instanceof WMSStoreInfo) {
WMSStoreInfo info = (WMSStoreInfo) store;
json.put("wms", info.getCapabilitiesURL());
}
json.put("connection", connection );
json.put("error", IO.error( new JSONObj(), store.getError()));
if (store.isEnabled()) {
resources(store, json.putArray("resources"),req);
}
json.put("layer-count",layerCount(store));
return json;
}
int layerCount(StoreInfo store) throws IOException {
Catalog cat = geoServer.getCatalog();
WorkspaceInfo ws = store.getWorkspace();
Filter filter = and(equal("store", store), equal("namespace.prefix", ws.getName()));
int count=0;
try (CloseableIterator<ResourceInfo> layers = cat.list(ResourceInfo.class, filter);) {
while (layers.hasNext()) {
ResourceInfo r = layers.next();
for (LayerInfo l : cat.getLayers(r)) {
if (l != null) {
count++;
}
}
}
}
return count;
}
private JSONArr layers(ResourceInfo r, JSONArr list) throws IOException {
if (r != null) {
Catalog cat = geoServer.getCatalog();
for (LayerInfo l : cat.getLayers(r)) {
JSONObj obj = layer(list.addObject(), l, true);
}
}
return list;
}
private JSONArr layers(StoreInfo store, JSONArr list) throws IOException {
Catalog cat = geoServer.getCatalog();
WorkspaceInfo ws = store.getWorkspace();
Filter filter = and(equal("store", store), equal("namespace.prefix", ws.getName()));
try (CloseableIterator<ResourceInfo> layers = cat.list(ResourceInfo.class, filter);) {
while (layers.hasNext()) {
ResourceInfo r = layers.next();
for (LayerInfo l : cat.getLayers(r)) {
layer(list.addObject(), l,true);
}
}
}
return list;
}
@SuppressWarnings("unchecked")
private JSONArr resources(StoreInfo store, JSONArr list, HttpServletRequest req) throws IOException {
Catalog cat = geoServer.getCatalog();
WorkspaceInfo ws = store.getWorkspace();
for (String resource : listResources(store)) {
resource( list.addObject(), store, resource, req);
}
return list;
}
private JSONObj resource(JSONObj obj, StoreInfo store, String name, HttpServletRequest req) throws IOException {
obj.put("name", name);
if(store instanceof DataStoreInfo){
DataStoreInfo data = (DataStoreInfo) store;
@SuppressWarnings("rawtypes")
DataAccess dataStore = data.getDataStore(new NullProgressListener());
FeatureType schema;
org.geotools.data.ResourceInfo info;
if (dataStore instanceof DataStore) {
schema = ((DataStore) dataStore).getSchema(name);
info = ((DataStore) dataStore).getFeatureSource(name).getInfo();
} else {
NameImpl qname = new NameImpl(name);
schema = dataStore.getSchema(qname);
info = dataStore.getFeatureSource(qname).getInfo();
}
String title = info.getTitle() == null
? WordUtils.capitalize(name)
: info.getTitle();
String description = info.getDescription() == null ? "" : info.getDescription();
obj.put("title", title);
obj.put("description", description);
JSONArr keywords = obj.putArray("keywords");
keywords.raw().addAll( info.getKeywords() );
IO.bounds(obj.putObject("bounds"),info.getBounds());
IO.schema(obj.putObject("schema"), schema, false);
}
if(store instanceof CoverageStoreInfo){
CoverageStoreInfo data = (CoverageStoreInfo) store;
GridCoverageReader r = data.getGridCoverageReader(null, null);
obj.put("title", WordUtils.capitalize(name));
obj.put("description", "");
if( r instanceof GridCoverage2DReader){
GridCoverage2DReader reader = (GridCoverage2DReader) r;
CoordinateReferenceSystem crs = reader.getCoordinateReferenceSystem(name);
IO.schemaGrid(obj.putObject("schema"), crs, false);
}
else {
IO.schemaGrid( obj.putObject("schema"), AbstractGridFormat.getDefaultCRS(), false);
}
}
JSONArr layers = obj.putArray("layers");
Catalog cat = geoServer.getCatalog();
if (store instanceof CoverageStoreInfo) {
// coverage store does not respect native name so we search by id
for (CoverageInfo info : cat.getCoveragesByCoverageStore((CoverageStoreInfo) store)) {
layers( info, layers );
}
}
else {
Filter filter = and(equal("namespace.prefix", store.getWorkspace().getName()),equal("nativeName", name));
try (
CloseableIterator<ResourceInfo> published = cat.list(ResourceInfo.class, filter);
) {
while (published.hasNext()) {
ResourceInfo info = published.next();
if (!info.getStore().getId().equals(store.getId())) {
continue; // native name is not enough, double check store id
}
layers( info, layers );
}
}
}
return obj;
}
private JSONObj resource( JSONObj json, ResourceInfo info, boolean details){
json.put("name", info.getName())
.put("workspace", info.getStore().getWorkspace().getName() );
if( details ){
if (info instanceof FeatureTypeInfo) {
FeatureTypeInfo data = (FeatureTypeInfo) info;
try {
IO.schema(json.putObject("schema"), data.getFeatureType(),false);
} catch (IOException e) {
}
}
else if (info instanceof CoverageInfo ){
CoverageInfo data = (CoverageInfo) info;
IO.schemaGrid(json.putObject("schema"),data,false);
}
}
return json;
}
private JSONObj layer(JSONObj json, LayerInfo info, boolean details) {
if (details) {
IO.layer(json, info, null);
// Todo add URL
} else {
json.put("name", info.getName())
.put("workspace", info.getResource().getStore().getWorkspace().getName());
}
return json;
}
private Iterable<String> listResources(StoreInfo store) throws IOException {
if (store instanceof DataStoreInfo) {
return Iterables.transform(((DataStoreInfo) store).getDataStore(null).getNames(),
new Function<Name, String>() {
@Nullable
@Override
public String apply(@Nullable Name input) {
return input.getLocalPart();
}
});
}
else if (store instanceof CoverageStoreInfo) {
return Arrays.asList(((CoverageStoreInfo) store).getGridCoverageReader(null, null).getGridCoverageNames());
}
else if (store instanceof WMSStoreInfo) {
return Iterables.transform(((WMSStoreInfo) store).getWebMapServer(null).getCapabilities().getLayerList(),
new Function<Layer, String>() {
@Nullable
@Override
public String apply(@Nullable Layer input) {
return input.getName();
}
});
}
throw new IllegalStateException("Unrecognized store type");
}
private String source(StoreInfo store) {
if( store instanceof CoverageStoreInfo ){
CoverageStoreInfo coverage = (CoverageStoreInfo) store;
return sourceURL( coverage.getURL() );
}
GeoServerResourceLoader resourceLoader = geoServer.getCatalog().getResourceLoader();
Map<String, Serializable> params =
ResourcePool.getParams( store.getConnectionParameters(), resourceLoader );
if( params.containsKey("dbtype")){
// See JDBCDataStoreFactory for details
String host = Converters.convert(params.get("host"), String.class);
String port = Converters.convert(params.get("port"), String.class);
String dbtype = Converters.convert(params.get("dbtype"), String.class);
String schema = Converters.convert(params.get("schema"), String.class);
String database = Converters.convert(params.get("database"), String.class);
StringBuilder source = new StringBuilder();
source.append(host);
if( port != null ){
source.append(':').append(port);
}
source.append('/').append(dbtype).append('/').append(database);
if( schema != null ){
source.append('/').append(schema);
}
return source.toString();
}
else if( store instanceof WMSStoreInfo){
String url = ((WMSStoreInfo)store).getCapabilitiesURL();
return url;
}
else if( params.keySet().contains("directory")){
String directory = Converters.convert(params.get("directory"),String.class);
return sourceFile( directory );
}
else if( params.keySet().contains("file")){
String file = Converters.convert(params.get("file"),String.class);
return sourceFile( file );
}
if( params.containsKey("url")){
String url = Converters.convert(params.get("url"),String.class);
return sourceURL( url );
}
for( Object value : params.values() ){
if( value instanceof URL ){
return source( (URL) value );
}
if( value instanceof File ){
return source( (File) value );
}
if( value instanceof String ){
String text = (String) value;
if( text.startsWith("file:")){
return sourceURL( text );
}
else if ( text.startsWith("http:") || text.startsWith("https:") || text.startsWith("ftp:")){
return text;
}
}
}
return "undertermined";
}
private String source(File file) {
File baseDirectory = dataDir().getResourceLoader().getBaseDirectory();
return file.isAbsolute() ? file.toString() : Paths.convert(baseDirectory,file);
}
private String source(URL url) {
File baseDirectory = dataDir().getResourceLoader().getBaseDirectory();
if (url.getProtocol().equals("file")) {
File file = Files.url(baseDirectory, url.toExternalForm());
if (file != null && !file.isAbsolute()) {
return Paths.convert(baseDirectory, file);
}
}
return url.toExternalForm();
}
private String sourceURL(String url) {
File baseDirectory = dataDir().getResourceLoader().getBaseDirectory();
File file = Files.url(baseDirectory, url);
if( file != null ){
return Paths.convert(baseDirectory, file);
}
return url;
}
private String sourceFile(String file) {
File baseDirectory = dataDir().getResourceLoader().getBaseDirectory();
File f = new File( file );
return f.isAbsolute() ? file : Paths.convert(baseDirectory, f);
}
private WorkspaceInfo findWorkspace(String wsName) {
Catalog cat = geoServer.getCatalog();
WorkspaceInfo ws;
if ("default".equals(wsName)) {
ws = cat.getDefaultWorkspace();
} else {
ws = cat.getWorkspaceByName(wsName);
}
if (ws == null) {
throw new RuntimeException("Unable to find workspace " + wsName);
}
return ws;
}
}