/* (c) 2014-2015 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.equal;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import org.apache.wicket.util.file.File;
import org.geoserver.catalog.CascadeDeleteVisitor;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CatalogFactory;
import org.geoserver.catalog.CoverageStoreInfo;
import org.geoserver.catalog.DataStoreInfo;
import org.geoserver.catalog.LayerGroupInfo;
import org.geoserver.catalog.Predicates;
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.platform.resource.Files;
import org.geotools.data.DataAccess;
import org.geotools.data.DataAccessFinder;
import org.geotools.data.DataStore;
import org.geotools.data.FeatureSource;
import org.geotools.data.Query;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.data.wms.WebMapServer;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.feature.NameImpl;
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.Feature;
import org.opengis.feature.type.FeatureType;
import org.opengis.filter.Filter;
import org.opengis.filter.sort.SortBy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
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 org.springframework.web.bind.annotation.ResponseStatus;
import com.boundlessgeo.geoserver.json.JSONArr;
import com.boundlessgeo.geoserver.json.JSONObj;
/**
* 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);
}
/**
* API endpoint to list the stores in the workspace
* @param wsName The workspace
* @param page Page of the list
* @param count Number of items per page
* @param sort Sort order (asc or desc)
* @param textFilter Search filter to limit results
* @param req The HTTP request
* @return A JSONObj containing the current page, the number of items returned, the total number
* of stores matching the current filter, and the list of stores for the current page.
*/
@RequestMapping(value = "/{wsName:.+}", method = RequestMethod.GET)
public @ResponseBody
JSONObj list(@PathVariable String wsName,
@RequestParam(value="page", required=false) Integer page,
@RequestParam(value="count", required=false, defaultValue=""+DEFAULT_PAGESIZE) Integer count,
@RequestParam(value="sort", required=false) String sort,
@RequestParam(value="filter", required=false) String textFilter,
HttpServletRequest req) {
Catalog cat = geoServer.getCatalog();
if ("default".equals(wsName)) {
WorkspaceInfo def = cat.getDefaultWorkspace();
if (def != null) {
wsName = def.getName();
}
}
Filter filter = equal("workspace.name", wsName);
if (textFilter != null) {
filter = Predicates.and(filter, Predicates.fullTextSearch(textFilter));
}
SortBy sortBy = parseSort(sort);
Integer total = cat.count(StoreInfo.class, filter);
JSONObj obj = new JSONObj();
obj.put("total", total);
obj.put("page", page != null ? page : 0);
obj.put("count", Math.min(total, count != null ? count : total));
JSONArr arr = obj.putArray("stores");
try (
CloseableIterator<StoreInfo> it =
cat.list(StoreInfo.class, filter, offset(page, count), count, sortBy);
) {
while (it.hasNext()) {
StoreInfo store = it.next();
IO.store(arr.addObject(), store, req, geoServer);
}
}
return obj;
}
/**
* API endpoint to get details on a specific store
* @param wsName The workspace name
* @param name The store name
* @param req The HTTP request
* @return The store, encoded as a JSON object
*/
@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 IO.storeDetails(new JSONObj(), store, req, geoServer);
} catch (IOException e) {
throw new RuntimeException(String.format("Error occured accessing store: %s,%s",wsName, name), e);
}
}
/**
* API endpoint to get details on a specific resource contained in a store,
* such as a database table.
* @param wsName The workspace name
* @param stName The store name
* @param name The resource name
* @param req The HTTP request
* @return The store, encoded as a JSON object
*/
@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 = IO.resource( new JSONObj(), store, name, geoServer);
obj.putObject("store")
.put("name", stName )
.put("url", IO.url(req, "/stores/%s/%s",wsName,stName));
return obj;
}
/**
* API endpoint to get the list of attributes of a specific resource contained in a store,
* such as a database table.
* @param wsName The workspace name
* @param stName The store name
* @param name The resource name
* @param req The HTTP request
* @return The schema and list of attributes, encoded as a JSON object
*/
@RequestMapping(value = "/{wsName}/{stName}/{name:.+}/attributes", method = RequestMethod.GET)
public @ResponseBody JSONObj attributes(
@PathVariable String wsName, @PathVariable String stName, @PathVariable String name,
@RequestParam(value="count", required=false, defaultValue=""+DEFAULT_PAGESIZE) Integer count,
HttpServletRequest req) throws IOException {
Catalog cat = geoServer.getCatalog();
StoreInfo store = findStore(wsName, stName, cat );
JSONObj obj = new JSONObj();
if (store instanceof DataStoreInfo) {
DataStoreInfo data = (DataStoreInfo) store;
@SuppressWarnings("rawtypes")
FeatureSource source;
@SuppressWarnings("rawtypes")
DataAccess dataStore = data.getDataStore(new NullProgressListener());
if (dataStore instanceof DataStore) {
source = ((DataStore) dataStore).getFeatureSource(name);
} else {
NameImpl qname = new NameImpl(name);
source = dataStore.getFeatureSource(qname);
}
//Limit number of features;
Query query = new Query(Query.ALL);
query.setMaxFeatures(count);
FeatureCollection features = source.getFeatures(query);
obj.put("schema", IO.schema(new JSONObj(), features.getSchema(), false));
obj.put("values", IO.features(new JSONArr(), features));
}
return obj;
}
/**
* API endpoint to delete a store from the catalog
* @param wsName The workspace name
* @param name The store name
* @param recurse Flag to recursively delete dependent maps and layers
* @param req The HTTP request
* @return The name and workspace of the deleted store
*/
@RequestMapping(value = "/{wsName}/{name:.+}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.NO_CONTENT)
public void 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){
store.accept(new CascadeDeleteVisitor(cat));
} 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() );
}
}
/**
* API endpoint to create a new store
* @param wsName The workspace to create the store in
* @param name The name of the new store
* @param obj The connection parameters for the store
* @param req The HTTP request
* @return The description of the newly created store
* @throws IOException
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@RequestMapping(value = "/{wsName}", method = RequestMethod.POST)
public @ResponseBody
JSONObj create(@PathVariable String wsName, @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();
// connect and defaults
info.setURL(url);
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();
// 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.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");
}
store.setWorkspace(workspace);
if (obj.get("name") != null) {
store.setName(obj.str("name"));
}
if (obj.get("type") != null) {
store.setType(obj.str("type"));
}
cat.add(store);
Metadata.created(store, new Date());
return IO.storeDetails(new JSONObj(), store, req, geoServer);
}
/**
* API endpoint to update an existing store
* @param wsName The workspace of the store
* @param name The name of the store
* @param obj Partial description of the store, containing all changes
* @param req HTTP request
* @return The description of the updated store
* @throws IOException
*/
@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);
Metadata.modified(store, new Date());
if (refresh) {
resetConnection(store);
}
return IO.storeDetails(new JSONObj(), store, req, geoServer);
}
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);
}
}
/**
* API endpoint to update an existing store
* @param wsName The workspace of the store
* @param name The name of the store
* @param obj Partial description of the store, containing all changes
* @param req HTTP request
* @return The description of the updated store
* @throws IOException
*/
@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 );
Metadata.modified(store, new Date());
if (refresh) {
resetConnection(store);
}
return IO.storeDetails(new JSONObj(), store, req, geoServer);
}
@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 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;
}
}