Java tutorial
/* (c) 2014-2015 Boundless, http://boundlessgeo.com * This code is licensed under the GPL 2.0 license. */ package com.boundlessgeo.geoserver.api.controllers; import com.boundlessgeo.geoserver.api.exceptions.BadRequestException; import com.boundlessgeo.geoserver.api.exceptions.NotFoundException; import com.boundlessgeo.geoserver.json.JSONArr; import com.boundlessgeo.geoserver.json.JSONObj; import com.boundlessgeo.geoserver.util.Hasher; import com.boundlessgeo.geoserver.util.NameUtil; import com.google.common.collect.Maps; import org.apache.commons.fileupload.FileItemIterator; import org.apache.commons.fileupload.FileItemStream; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.geoserver.catalog.CascadeDeleteVisitor; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.CoverageStoreInfo; import org.geoserver.catalog.DataStoreInfo; import org.geoserver.catalog.LayerInfo; import org.geoserver.catalog.ProjectionPolicy; import org.geoserver.catalog.ResourceInfo; import org.geoserver.catalog.ResourcePool; import org.geoserver.catalog.StoreInfo; import org.geoserver.catalog.StyleInfo; import org.geoserver.catalog.WorkspaceInfo; import org.geoserver.config.GeoServer; import org.geoserver.config.GeoServerDataDirectory; import org.geoserver.importer.ASpatialFile; import org.geoserver.importer.Database; import org.geoserver.importer.Directory; import org.geoserver.importer.FileData; import org.geoserver.importer.ImportContext; import org.geoserver.importer.ImportData; import org.geoserver.importer.ImportFilter; import org.geoserver.importer.ImportTask; import org.geoserver.importer.ImportTask.State; import org.geoserver.importer.Importer; import org.geoserver.importer.SpatialFile; import org.geoserver.importer.Table; import org.geoserver.importer.job.Task; import org.geoserver.platform.ContextLoadedEvent; import org.geoserver.platform.resource.Paths; import org.geoserver.ows.util.ResponseUtils; import org.geoserver.platform.resource.Resource; import org.geoserver.rest.util.RESTUploadExternalPathMapper; import org.geoserver.rest.util.RESTUploadPathMapper; import org.geoserver.rest.util.RESTUtils; import org.geoserver.ysld.YsldHandler; import org.geotools.data.DataAccessFactory.Param; import org.geotools.data.DataStoreFactorySpi; import org.geotools.util.logging.Logging; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; 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.ResponseBody; import org.vfny.geoserver.util.DataStoreUtils; import javax.servlet.http.HttpServletRequest; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.nio.file.Files; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; @Controller @RequestMapping("/api/imports") public class ImportController extends ApiController { Importer importer; Hasher hasher; private Long id = 0L; private Map<Long, ImportHelper> imports = new HashMap<Long, ImportHelper>(); private static Logger LOG = Logging.getLogger(ImportController.class); @Autowired public ImportController(GeoServer geoServer, ApplicationContext ctx) { super(geoServer); this.importer = (Importer) ctx.getBean("composer-importer"); this.importer.setStyleHandler(new YsldHandler()); this.hasher = new Hasher(7); } /** * API endpoint to get space available info when uploading files * * TODO: If a dedicated file/resource API controller gets created, migrate this method to there * TODO: If/when workspace-specific upload limitations are implemented, update this to be * workspace-specific * @return A JSON object containing information about the space available in the workspace * @throws IOException */ @RequestMapping(value = "/{wsName:.+}", method = RequestMethod.GET) public @ResponseBody JSONObj info(@PathVariable String wsName) throws IOException { JSONObj obj = new JSONObj(); Catalog catalog = geoServer.getCatalog(); WorkspaceInfo ws = findWorkspace(wsName, catalog); if (ws != null) { obj.put("workspace", ws.getName()); } //Temp dir File tmpDir = new File(System.getProperty("java.io.tmpdir")); //Global REST upload dir String externalRoot = null; if (externalRoot == null) { externalRoot = RESTUtils.extractMapItem( RESTUtils.loadMapfromWorkSpace(ws == null ? null : ws.getName(), catalog), RESTUtils.ROOT_KEY); } if (externalRoot == null) { externalRoot = RESTUtils.extractMapItem(RESTUtils.loadMapFromGlobal(), RESTUtils.ROOT_KEY); } if (externalRoot == null) { externalRoot = Paths.toFile(catalog.getResourceLoader().getBaseDirectory(), Paths.path("data", ws == null ? null : ws.getName())).getAbsolutePath(); } File destDir = new File(externalRoot); long freeSpace = 0; if (tmpDir.exists()) { freeSpace = tmpDir.getUsableSpace(); obj.put("tmpDir", tmpDir.getPath()); obj.put("tmpSpace", freeSpace); } if (destDir.exists()) { freeSpace = destDir.getUsableSpace(); obj.put("uploadDir", destDir.getPath()); obj.put("uploadSpace", freeSpace); obj.put("spaceUsed", FileUtils.sizeOfDirectory(destDir)); } //In case the destDir is on a different filesystem than the tmpDir, take the min of both if (tmpDir.exists() && destDir.exists()) { freeSpace = Math.min(tmpDir.getUsableSpace(), destDir.getUsableSpace()); } obj.put("spaceAvailable", freeSpace); return obj; } /** * API endpoint to import a file or list of files as a new layer or layers into GeoServer. * Files are provided as MediaType.MULTIPART_FORM_DATA_VALUE in the request * @param wsName The workspace to import the files into * @param request The HTTP request * @return a JSON object describing the result of the import. See {@link #get(String, Long) get}. * @throws Exception if the request is invalid, or the file upload fails. */ @RequestMapping(value = "/{wsName:.+}", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public @ResponseBody JSONObj importFile(@PathVariable String wsName, HttpServletRequest request) throws Exception { return importFile(wsName, null, request); } /** * API endpoint to import a file or list of files as a new layer or layers into into an existing * store inGeoServer. * Files are provided as MediaType.MULTIPART_FORM_DATA_VALUE in the request * @param wsName The workspace to import the files into * @param storeName The store to import the layers into. If null, tries to import into a new * store. * @param request The HTTP request * @return a JSON object describing the result of the import. See {@link #get(String, Long) get}. * @throws Exception if the request is invalid, or the file upload fails. */ @RequestMapping(value = "/{wsName:.+}/{storeName:.+}", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public @ResponseBody JSONObj importFile(@PathVariable String wsName, @PathVariable String storeName, HttpServletRequest request) throws Exception { // grab the workspace Catalog catalog = geoServer.getCatalog(); WorkspaceInfo ws = findWorkspace(wsName, catalog); // get the uploaded files FileItemIterator files = doFileUpload(request); if (!files.hasNext()) { throw new BadRequestException("Request must contain one or more files"); } // create a new temp directory for the uploaded file File uploadDir = Files.createTempDirectory("importFile").toFile(); if (!uploadDir.exists()) { throw new RuntimeException("Unable to create directory for file upload"); } uploadDir.deleteOnExit(); // pass off the uploaded file(s) to the importer Directory dir = new Directory(uploadDir); while (files.hasNext()) { FileItemStream item = files.next(); try (InputStream stream = item.openStream()) { String name = item.getName(); dir.accept(name, stream); } } Long id; if (storeName == null) { id = importer.createContextAsync(dir, ws, null); } else { StoreInfo store = findStore(wsName, storeName, geoServer.getCatalog()); id = importer.createContextAsync(dir, ws, store); } return get(wsName, createImport(importer.getTask(id)), request); } public static File uploadDir(Catalog catalog, WorkspaceInfo ws, StoreInfo store) throws IOException { RESTUploadPathMapper destDirMapper = new RESTUploadExternalPathMapper(catalog); //Match org.catalog.rest.StoreFileResource default base dir StringBuilder destDirBuilder = new StringBuilder( Paths.toFile(catalog.getResourceLoader().getBaseDirectory(), Paths.path("data", ws.getName(), store.getName())).getAbsolutePath()); //See if we can get the root directory from the Global REST root path destDirMapper.mapStorePath(destDirBuilder, ws.getName(), store.getName(), new HashMap<String, String>()); //Move main file return new File(destDirBuilder.toString()); } /** * Moves uploaded files from the temp upload directory to the appropriate store folder * Updates the ImportContext tasks and stores with the new location. * Updates the catalog store with the new location. * * @param t ImportTask containing data about a single import */ void moveFile(ImportTask t) { Catalog catalog = geoServer.getCatalog(); StoreInfo store = catalog.getStore(t.getStore().getId(), StoreInfo.class); if (store == null) { LOG.warning("Trying to move files to a non-existant store"); return; } if (!(t.getData() instanceof FileData)) { LOG.warning("Trying to move non-file data"); return; } if (!t.isDirect()) { LOG.warning("Trying to move files from an indirect import"); return; } WorkspaceInfo ws = store.getWorkspace(); //Special behavior for SpatialFile - linked files FileData srcData = (FileData) t.getData(); File srcFile = srcData.getFile().file(); File storeFile; File destDir; FileData destData; File destFile; try { destDir = uploadDir(catalog, ws, store); destFile = new File(destDir, srcData.getFile().file().getName()); if (srcFile.getAbsoluteFile().equals(destFile.getAbsoluteFile())) { LOG.warning("Trying to move file to itself"); return; } destDir.mkdirs(); //Update Store File baseDirectory = catalog.getResourceLoader().getBaseDirectory(); if (store instanceof CoverageStoreInfo) { storeFile = catalog.getResourceLoader().url(((CoverageStoreInfo) store).getURL()); //A CoverageStore needs a single file String url = "file:" + Paths.convert(baseDirectory, destFile); if (!(srcData.getFile().file().getAbsolutePath().equals(storeFile.getAbsolutePath()))) { throw new RuntimeException("CoverageStore file not the same as imported file"); } ((CoverageStoreInfo) store).setURL(url); } else if (store instanceof DataStoreInfo) { storeFile = catalog.getResourceLoader().url(store.getConnectionParameters().get("url").toString()); /* A DataStore may contain multiple files as separate "tables". * Therefore, we use the store dir for the URL, and ensure the file location is * somewhere in this directory. * * If the store file is the same as the destination directory, then we may be in * progress moving files. * * If the store file is a prefix of the source data file, then all is well * * Otherwise, we have a problem and should abort. */ String url = "file:" + Paths.convert(baseDirectory, destDir); if (!(storeFile.equals(destDir.getAbsoluteFile()) || srcData.getFile().file().getAbsolutePath().startsWith(storeFile.getAbsolutePath()))) { throw new RuntimeException("DataStore file not the same as imported file"); } store.getConnectionParameters().put("url", url); } else { throw new RuntimeException("Invalid store type: " + store.getClass()); } //Test if we can actually move the file; otherwise copy the file. boolean move = srcFile.renameTo(srcFile); if (move) { Files.move(srcFile.toPath(), destFile.toPath()); } else { Files.copy(srcFile.toPath(), destFile.toPath()); } //move any supplementary files, update ImportData if (srcData instanceof SpatialFile) { destData = new SpatialFile(destFile); if (((SpatialFile) srcData).getPrjFile() != null) { File prjFile = new File(destDir, ((SpatialFile) srcData).getPrjFile().file().getName()); if (move) { Files.move(((SpatialFile) srcData).getPrjFile().file().toPath(), prjFile.toPath()); } else { Files.copy(((SpatialFile) srcData).getPrjFile().file().toPath(), prjFile.toPath()); } ((SpatialFile) destData).setPrjFile(org.geoserver.platform.resource.Files.asResource(prjFile)); } for (Resource r : ((SpatialFile) srcData).getSuppFiles()) { File suppFile = new File(destDir, r.file().getName()); if (move) { Files.move(r.file().toPath(), suppFile.toPath()); } else { Files.copy(r.file().toPath(), suppFile.toPath()); } ((SpatialFile) destData).getSuppFiles() .add(org.geoserver.platform.resource.Files.asResource(suppFile)); } } else if (srcData instanceof ASpatialFile) { destData = new ASpatialFile(org.geoserver.platform.resource.Files.asResource(destFile)); } else { destData = new FileData(org.geoserver.platform.resource.Files.asResource(destFile)); } } catch (Exception e) { //If this occurs, the store files will be in a temporary folder, so we should abort the import t.setError(e); t.setState(State.ERROR); store.accept(new CascadeDeleteVisitor(catalog)); throw new RuntimeException("Failed to move imported files to uploads directory", e); } //Copy over attributes from srcData destData.setFormat(srcData.getFormat()); destData.setMessage(srcData.getMessage()); destData.setCharsetEncoding(srcData.getCharsetEncoding()); //Update data t.setData(destData); t.setStore(store); //Save the updated store to the catalog catalog.save(store); } /** * API endpoint to import a database as a new layer or layers into GeoServer. * Connection details are provided as a JSONObject, formatted according to the * {@see com.boundlessgeo.geoserver.api.controllers.FormatController} API. * @param wsName The workspace to import the database into * @param request The HTTP request * @return a JSON object describing the result of the import. See {@link #get(String, Long) get}. * @throws Exception if the request is invalid. */ @RequestMapping(value = "/{wsName:.+}", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) public @ResponseBody JSONObj importDb(@PathVariable String wsName, @RequestBody JSONObj obj, HttpServletRequest request) throws Exception { return importDb(wsName, null, obj, request); } /** * API endpoint to import the contents of a database as a new layer or layers into an existing * data store in GeoServer. * Connection details are provided as a JSONObject, formatted according to the * {@see com.boundlessgeo.geoserver.api.controllers.FormatController} API. * @param wsName The workspace to import the database into * @param storeName The store to import the layers into. If null, tries to import into a new * store. * @param request The HTTP request * @return a JSON object describing the result of the import. See {@link #get(String, Long) get}. * @throws Exception if the request is invalid. */ @RequestMapping(value = "/{wsName:.+}/{storeName:.+}", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) public @ResponseBody JSONObj importDb(@PathVariable String wsName, @PathVariable String storeName, @RequestBody JSONObj obj, HttpServletRequest request) throws Exception { // grab the workspace Catalog catalog = geoServer.getCatalog(); WorkspaceInfo ws = findWorkspace(wsName, catalog); // create the import data Database db = new Database(hack(obj)); Long id; if (storeName == null) { id = importer.createContextAsync(db, ws, null); //Check if this store already exists in the catalog StoreInfo store = findStore(hack(obj), ws); if (store != null) { return (new JSONObj()).put("store", IO.store(new JSONObj(), store, request, geoServer)); } } else { StoreInfo store = findStore(wsName, storeName, geoServer.getCatalog()); id = importer.createContextAsync(db, ws, store); } //Return to requester to allow selection of tables. //Complete the import using update() return get(ws.getName(), createImport(importer.getTask(id)), request); } Map<String, Serializable> hack(JSONObj obj) { Map<String, Serializable> map = Maps.newLinkedHashMap(); for (Object e : obj.raw().entrySet()) { Map.Entry<String, Serializable> entry = (Map.Entry) e; Serializable value = entry.getValue(); if (value instanceof Long) { value = ((Long) value).intValue(); } map.put(entry.getKey(), value); } return map; } /* * Extracts details on a task filter from the JSONObj and creates a ImportFilter object based on these * details. Valid filters are: * * "filter":"ALL" - All tasks * * "tasks":[<list of tasks>] where each task object must contain "task":<task id> - Only * select the tasks identified in the list */ ImportFilter filter(JSONObj obj, ImportContext imp) { Object filter = obj.get("filter"); JSONArr arr = obj.array("tasks"); if (filter == null && arr == null) { throw new BadRequestException("Request must contain a filter or a list of tasks"); } if (arr == null) { if ("ALL".equals(filter)) { return ImportFilter.ALL; } throw new BadRequestException("Invalid filter: " + filter); } else { final List<Long> tasks = new ArrayList<Long>(arr.size()); for (int i = 0; i < arr.size(); i++) { JSONObj taskObj = arr.object(i); Long taskId = Long.parseLong(taskObj.str("task")); if (taskId == null) { throw new BadRequestException("Request must contain task identifier"); } ImportTask task = imp.task(taskId); if (task == null) { throw new NotFoundException("No such task: " + taskId + " for import: " + imp.getId()); } tasks.add(taskId); } return new TaskIdFilter(tasks); } } /** * Runs an import using the GeoServer importer * @param data - The data to import * @param ws - The workspace to import into * @return JSON representation of the import */ JSONObj doImport(ImportHelper helper, WorkspaceInfo ws, HttpServletRequest request) throws Exception { return doImport(helper, ws, ImportFilter.ALL, request); } /** * Runs an import using the GeoServer importer * @param helper - The ImportWrapper containing the context to run the import from * @param ws - The workspace to import into * @param f - Filter to select import tasks * @return JSON representation of the import */ JSONObj doImport(ImportHelper helper, WorkspaceInfo ws, ImportFilter f, HttpServletRequest request) throws Exception { helper.setTask(null); ImportContext imp = helper.getContext(); // run the import imp.setState(ImportContext.State.RUNNING); GeoServerDataDirectory dataDir = dataDir(); for (ImportTask t : imp.getTasks()) { if (f.include(t)) { prepTask(t, ws, dataDir); } } helper.setTask(importer.getTask(importer.runAsync(imp, f, false))); return get(ws.getName(), helper.id, request); } /* * 1. hack the context object to ensure that all styles are workspace local * 2. mark layers as "imported" so we can safely delete styles later */ void prepTask(ImportTask t, WorkspaceInfo ws, GeoServerDataDirectory dataDir) { LayerInfo l = t.getLayer(); //If the task has no associated layer, it is probably a junk file that won't get imported if (l == null) { return; } l.getMetadata().put(Metadata.IMPORTED, new Date()); l.getResource().setProjectionPolicy(ProjectionPolicy.FORCE_DECLARED); //If the style exists, and we haven't already moved it into the workspace, move it to ws. if (l != null && l.getDefaultStyle() != null && !ws.equals(l.getDefaultStyle().getWorkspace())) { StyleInfo s = l.getDefaultStyle(); // JD: have to regenerate the unique name here, the importer already does this but because we are // putting it into the workspace we have to redo it, this should really be part of the importer // with an option to create styles in the workspace s.setName(findUniqueStyleName(l.getResource(), ws, catalog())); Resource from = dataDir.style(s); s.setWorkspace(ws); Resource to = dataDir.style(s); try { try (InputStream in = from.in(); OutputStream out = to.out();) { IOUtils.copy(in, out); from.delete(); } } catch (IOException e) { throw new RuntimeException("Error copying style to workspace", e); } } } String findUniqueStyleName(ResourceInfo resource, WorkspaceInfo workspace, Catalog catalog) { String styleName = resource.getName(); StyleInfo style = catalog.getStyleByName(workspace, styleName); int i = 1; while (style != null) { styleName = resource.getName() + i; style = catalog.getStyleByName(workspace, styleName); i++; } return styleName; } /** * Get details on an existing import. * @param wsName The workspace of the import * @param id The id of the import * @return A JSONObj containing the import details: * { * "id": 0, * "task": "Processing data usa-merc.tif" * "importerEndpoint":"http:\/\/localhost:8000\/geoserver\/geoserver\/rest\/imports\/1", * "preimport": [], * "running": [], * "imported": [ * { * "task": 0, * "name": "stipple.shp", * "type": "file" * "layer": { * "name": "stipple", * "workspace": "opengeo", * ... rest of layer definition * } * } * ], * "pending": [ * { * "task": 1, * "name": "usa-merc.tif", * "type": "file" * "problem": "NO_CRS" * } * ], * "ignored": [], * "failed": [] * "status":"complete" * "tasksComplete":2 * "tasksTotal":2 * } * * The response is an object with 7 properties. If this is a new import, and the context has * not yet been created, only id and task will be included. * * id - The identifier of this import job, needed to make modifications to it * * task - A textual representation of the current task. Not included if no task is currently running. * * importerEndpoint - REST endpoint to manage this import using the GeoServer Importer REST API * * status - The current status of the import. May be "pending", "running", or "complete" * * tasksComplete - The number of tasks that have been processed and have either been * successfully imported or given an error. * * tasksTotal - The total number of tasks associated with this import * * tasks - An array of tasks. See {@see #task(ImportTask)}. * * @throws Exception if the request is invalid, or another error occurs. */ @RequestMapping(value = "/{wsName}/{id:\\d+}", method = RequestMethod.GET) public @ResponseBody JSONObj get(@PathVariable String wsName, @PathVariable Long id, HttpServletRequest request) throws Exception { ImportHelper helper = imports.get(id); if (helper == null) { throw new NotFoundException("Import with id " + id + " does not exist"); } JSONObj result = new JSONObj().put("id", id); Task<ImportContext> t = helper.getTask(); ImportContext imp = helper.getContext(); if (t != null) { result.put("task", t.toString()); } if (imp == null) { if (t == null) { throw new RuntimeException("Invalid import"); } return result; } //Remove this while GeoServer and Composer use seperate Importer objects //result.put("importerEndpoint", ResponseUtils.baseURL(request)+"geoserver/rest/imports/"+imp.getId()); JSONArr tasks = result.putArray("tasks"); int completed = 0; int pending = 0; for (ImportTask task : imp.getTasks()) { tasks.add(task(task)); switch (task.getState()) { case READY: case PENDING: case RUNNING: pending++; break; default: completed++; break; } } result.put("tasksCompleted", completed); result.put("tasksTotal", imp.getTasks().size()); //If there are no more tasks to run, consider the import complete if (pending == 0) { imp.setState(ImportContext.State.COMPLETE); } switch (imp.getState()) { case COMPLETE: result.put("state", "complete"); break; case PENDING: result.put("state", "pending"); break; case RUNNING: result.put("state", "running"); break; default: break; } return result; } /** * Modify an existing import. This can involve updating projections, or importing previously * excluded tasks. * * @param wsName * @param id * @param obj The update request: * The request payload can be a list of task objects with any additional data that needs to be * specified by the user. This example (re)imports of task 0, and sets the projection and * re-imports task 1: * { * "tasks":[ * { * "task": 0 * }, * { * "task": 1, * "proj": "epsg:3857" * }] * } * Alternatively, the request can specify a filter to select tasks to import. * Currently "ALL" is the only supported filter. This would import all tasks: * { "filter":"ALL" } * * If the import is currently running, you cannot update it, as this may interfere with running * tasks and cause an inconsistent state. To cancel a running import, provide the value * "cancelled":true in the request object. * * @return a JSON object describing the result of the import. See {@link #get(String, Long) get}. * @throws Exception if the request is invalid. */ @RequestMapping(value = "/{wsName}/{id:\\d+}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE) public @ResponseBody JSONObj update(@PathVariable String wsName, @PathVariable Long id, @RequestBody JSONObj obj, HttpServletRequest request) throws Exception { Catalog catalog = geoServer.getCatalog(); WorkspaceInfo ws = findWorkspace(wsName, catalog); ImportHelper helper = imports.get(id); if (helper == null) { throw new NotFoundException("Import with id " + id + " does not exist"); } JSONObj result = new JSONObj().put("id", id); Task<ImportContext> t = helper.getTask(); ImportContext imp = helper.getContext(); //If this import is currently running, we should not start another job if (t != null) { if (obj.bool("cancelled")) { t.cancel(true); return get(wsName, id, request); } else { throw new RuntimeException("This import is currently running."); } } if (imp == null) { throw new RuntimeException("Invalid import"); } ImportFilter f = filter(obj, imp); JSONArr arr = obj.array("tasks"); //Filter only: run on all tasks that match the filter if (arr == null) { return doImport(helper, ws, f, request); } //Task List: Update CRS tasks, run all tasks that match filter or list. for (int i = 0; i < arr.size(); i++) { JSONObj taskObj = arr.object(i); Long taskId = Long.parseLong(taskObj.str("task")); if (taskId == null) { throw new BadRequestException("Request must contain task identifier"); } ImportTask task = imp.task(taskId); if (task == null) { throw new NotFoundException("No such task: " + taskId + " for import: " + id); } ResourceInfo resource = task.getLayer().getResource(); JSONObj proj = taskObj.object("proj"); if (task.getState() == ImportTask.State.NO_CRS && proj == null) { throw new BadRequestException("Task " + taskId + " requires a 'proj' property"); } if (proj != null) { try { resource.setSRS(IO.srs(proj)); resource.setNativeCRS(IO.crs(proj)); importer.changed(task); } catch (Exception e) { throw new BadRequestException("Unable to parse proj: " + proj.toString()); } } } return doImport(helper, ws, f, request); } ImportContext findImport(Long id) { ImportContext imp = importer.getContext(id); if (imp == null) { throw new NotFoundException("No such import: " + id); } return imp; } /* * Searches the catalog for existing stores that have the same connection parameters * as those described by the DataFormat of this ImportContext, in order to test if the store * that would be created by this import already exists. * * For better matching, only compares the required parameters of the store. */ StoreInfo findStore(Map<String, Serializable> params, WorkspaceInfo ws) throws Exception { Catalog catalog = geoServer.getCatalog(); Map<String, Serializable> requiredParams = new HashMap<String, Serializable>(); DataStoreFactorySpi factory = (DataStoreFactorySpi) DataStoreUtils.aquireFactory(params); if (factory == null) { return null; } for (Param p : factory.getParametersInfo()) { if (p.isRequired() && params.get(p.getName()) != null) { requiredParams.put(p.getName(), params.get(p.getName())); } } //Special case to support connections to different databases on the same database server if (params.get("database") != null) { requiredParams.put("database", params.get("database")); } List<? extends StoreInfo> stores = catalog.getStoresByWorkspace(ws, StoreInfo.class); for (StoreInfo s : stores) { boolean matches = true; Map<String, Serializable> p = ResourcePool.getParams(s.getConnectionParameters(), catalog.getResourceLoader()); for (String key : requiredParams.keySet()) { //On-disk params read as strings, so compare as strings if (!(requiredParams.get(key).toString()) .equals((p.get(key) == null ? "" : p.get(key).toString()))) { matches = false; } } if (matches) { return s; } } return null; } void touch(ImportTask task) { LayerInfo l = task.getLayer(); l = catalog().getLayer(l.getId()); if (l != null) { Date now = new Date(); //If we have already touched this task, don't update if (Metadata.created(l) == null) { Metadata.created(l, now); } Metadata.modified(l, now); geoServer.getCatalog().save(l); } } /** * A json representation of an importer task * * The following attributes are included: * "task": task id * "name": task name * "status': task status. One of READY, PENDING, RUNNING, NO_BOUNDS, NO_CRS, COMPLETE, ERROR, or IGNORED * "layer": details of the imported layer. Only applicable for "status":COMPLETE * "error": details of the error. Only applicable for "status":ERROR * "type": data type (file | database | table) of the task. Not applicable for "status":IGNORED * "geometry": geometry type ({@see com.boundlessgeo.geoserver.api.controllers.IO#geometry(LayerInfo) }) of the task. Not applicable for "status":IGNORED * @param task * @return */ JSONObj task(ImportTask task) { JSONObj obj = new JSONObj(); obj.put("task", task.getId()).put("name", name(task)); switch (task.getState()) { case READY: case PENDING: case RUNNING: case NO_BOUNDS: case NO_CRS: obj.put("status", task.getState().toString()); break; case COMPLETE: obj.put("status", task.getState().toString()); touch(task); LayerInfo layer = task.getLayer(); IO.layerDetails(obj.putObject("layer"), layer, null); break; case ERROR: // error, dump out some details obj.put("status", task.getState().toString()); IO.error(obj.putObject("error"), task.getError()); break; default: obj.put("status", "IGNORED"); //no type or geometry return obj; } obj.put("type", type(task)).put("geometry", IO.geometry(task.getLayer())); return obj; } JSONObj complete(ImportTask task) { touch(task); LayerInfo layer = task.getLayer(); JSONObj obj = task(task); IO.layerDetails(obj.putObject("layer"), layer, null); return obj; } JSONObj pending(ImportTask task) { return task(task).put("problem", task.getState().toString()); } JSONObj failed(ImportTask task) { JSONObj err = task(task); IO.error(err, task.getError()); return err; } JSONObj ignored(ImportTask task) { return new JSONObj().put("task", task.getId()).put("name", name(task)); } String name(ImportTask task) { ImportData data = task.getData(); if (data instanceof FileData) { return FilenameUtils.getName(data.toString()); } return data.getName(); } String type(ImportTask task) { ImportData data = task.getData(); if (data instanceof Database) { return "database"; } if (data instanceof FileData) { return "file"; } if (data instanceof Table) { return "table"; } return "null"; } static class TaskIdFilter implements ImportFilter { List<Long> tasks; public TaskIdFilter(List<Long> tasks) { this.tasks = tasks; } @Override public boolean include(ImportTask task) { return tasks.contains(task.getId()); } } /** * Create a new ImportHelper and add it to {@code imports}, tracking the index. * @param t * @return */ private Long createImport(Task<ImportContext> t) { ImportHelper i = new ImportHelper(t); //Make sure we get the right index synchronized (id) { imports.put(id, i); i.id = id; id++; } return i.id; } /** * Utility class to track both the current Task, and the ImportContext with a consistent id. * The intended use pattern is to use {@link #createImport} to create the wrapper with the Task * object created by importer.createContextAsync. When this task has finished execution, * this class is updated with the newly created context. * Subsequent tasks can be added with the setTask method. * */ private class ImportHelper { private Task<ImportContext> currentTask = null; private ImportContext context = null; Long id = -1L; protected ImportHelper(Task<ImportContext> initTask) { if (initTask == null) { throw new NullPointerException("Initial task cannot be null"); } currentTask = initTask; new Thread(new TaskListener(initTask)).start(); } public ImportContext getContext() { return context; } /** * Returns the currently running task. If the current task has completed, updates the * ImportContext and sets the current task to null. * @return The currently running task, or null if no task is running * @throws InterruptedException * @throws ExecutionException */ public Task<ImportContext> getTask() throws InterruptedException, ExecutionException { if (currentTask != null && currentTask.isDone()) { context = currentTask.get(); currentTask = null; } return currentTask; } /** * Sets the currentTask, as long as no task is currently running. * Applies a TaskListener to the new task, used to update file locations when an import completes. * @param t * @throws RuntimeException If a currentTask is already running * @throws InterruptedException * @throws ExecutionException */ public void setTask(Task<ImportContext> t) throws RuntimeException, InterruptedException, ExecutionException { if (t != null) { new Thread(new TaskListener(t)).start(); } if (getTask() == null) { currentTask = t; } else { throw new RuntimeException("Cannot set task: another task is still running"); } } } /** * Utility class which blocks on a running Task, and updates the location of any Imported files * when that task completes. */ private class TaskListener implements Runnable { Task<ImportContext> task; public TaskListener(Task<ImportContext> task) { this.task = task; } public void run() { try { ImportContext context = task.get(); for (ImportTask t : context.getTasks()) { if (t.getState() == State.COMPLETE) { //Clean up names Catalog catalog = ImportController.this.geoServer.getCatalog(); StoreInfo store = catalog.getStore(t.getStore().getId(), StoreInfo.class); store.setName(NameUtil.sanitizeEnsureUnique(store.getName(), StoreInfo.class, catalog)); LayerInfo layer = catalog.getLayer(t.getLayer().getId()); layer.setName(NameUtil.sanitizeEnsureUnique(layer.getName(), LayerInfo.class, catalog)); //ResourceInfo resource = catalog.getResource(layer.getResource().getId(), layer.getResource().getClass()); //resource.setName(NameUtil.sanitizeEnsureUnique(resource.getName(), ResourceInfo.class, catalog)); StyleInfo style = catalog.getStyle(layer.getDefaultStyle().getId()); style.setName(NameUtil.sanitizeEnsureUnique(style.getName(), StyleInfo.class, catalog)); catalog.save(store); catalog.save(layer); catalog.save(style); t.setStore(store); t.setLayer(layer); moveFile(t); //Set created date store = t.getStore(); if (store != null && Metadata.created(store) == null) { Metadata.created(store, new Date()); } } } } catch (InterruptedException | ExecutionException e) { LOG.log(Level.WARNING, "Failed to move imported files", e); } } } }