/* (c) 2017 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.rest.catalog; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.CatalogBuilder; import org.geoserver.catalog.CoverageInfo; import org.geoserver.catalog.CoverageStoreInfo; import org.geoserver.catalog.LayerInfo; import org.geoserver.catalog.SingleGridCoverage2DReader; import org.geoserver.data.util.CoverageStoreUtils; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.resource.Paths; import org.geoserver.platform.resource.Resource; import org.geoserver.platform.resource.Resource.Type; import org.geoserver.platform.resource.Resources; import org.geoserver.rest.ResourceNotFoundException; import org.geoserver.rest.RestBaseController; import org.geoserver.rest.RestException; import org.geoserver.rest.util.RESTUploadPathMapper; import org.geoserver.rest.wrapper.RestWrapper; import org.geotools.coverage.grid.io.AbstractGridFormat; import org.geotools.coverage.grid.io.GridCoverage2DReader; import org.geotools.coverage.grid.io.StructuredGridCoverage2DReader; import org.geotools.data.DataUtilities; import org.geotools.factory.GeoTools; import org.opengis.coverage.grid.Format; import org.opengis.coverage.grid.GridCoverageReader; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController @ControllerAdvice @RequestMapping(path = RestBaseController.ROOT_PATH + "/workspaces/{workspaceName}/coveragestores/{storeName}/{method}.{format}") public class CoverageStoreFileController extends AbstractStoreUploadController { /** * Keys every known coverage format by lowercase name */ protected static final HashMap<String, String> FORMAT_LOOKUP = new HashMap<>(); static { for (Format format : CoverageStoreUtils.formats) { FORMAT_LOOKUP.put(format.getName().toLowerCase(), format.getName()); } } @Autowired public CoverageStoreFileController(@Qualifier("catalog") Catalog catalog) { super(catalog); } @PostMapping @ResponseStatus(code = HttpStatus.ACCEPTED) public void coverageStorePost( @PathVariable String workspaceName, @PathVariable String storeName, @PathVariable UploadMethod method, @PathVariable String format, HttpServletRequest request) throws IOException { // check the coverage store exists CoverageStoreInfo info = catalog.getCoverageStoreByName(workspaceName, storeName); if (info == null) { throw new ResourceNotFoundException( "No such coverage store: " + workspaceName + "," + storeName); } GridCoverageReader reader = info.getGridCoverageReader(null, null); if (reader instanceof StructuredGridCoverage2DReader) { StructuredGridCoverage2DReader sr = (StructuredGridCoverage2DReader) reader; if (sr.isReadOnly()) { throw new RestException( "Coverage store found, but it cannot harvest extra resources", HttpStatus.METHOD_NOT_ALLOWED); } } else { throw new RestException( "Coverage store found, but it does not support resource harvesting", HttpStatus.METHOD_NOT_ALLOWED); } StructuredGridCoverage2DReader sr = (StructuredGridCoverage2DReader) reader; // This method returns a List of the harvested files. final List<File> uploadedFiles = new ArrayList<>(); for (Resource res : doFileUpload(method, workspaceName, storeName, format, request)) { uploadedFiles.add(Resources.find(res)); } // File Harvesting sr.harvest(null, uploadedFiles, GeoTools.getDefaultHints()); } @PutMapping(produces = { MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE }) @ResponseStatus(code = HttpStatus.CREATED) public RestWrapper<CoverageStoreInfo> coverageStorePut( @PathVariable String workspaceName, @PathVariable String storeName, @PathVariable UploadMethod method, @PathVariable String format, @RequestParam(name = "configure", required = false) String configure, @RequestParam(name = "USE_JAI_IMAGEREAD", required = false) Boolean useJaiImageRead, @RequestParam(name = "coverageName", required = false) String coverageName, HttpServletRequest request) throws IOException { Format coverageFormat = getCoverageFormat(format); // doFileUpload returns a List of File but in the case of a Put operation the list contains only a value List<Resource> files = doFileUpload(method, workspaceName, storeName, format, request); final Resource uploadedFile = files.get(0); // create a builder to help build catalog objects CatalogBuilder builder = new CatalogBuilder(catalog); builder.setWorkspace(catalog.getWorkspaceByName(workspaceName)); // create the coverage store CoverageStoreInfo info = catalog.getCoverageStoreByName(workspaceName, storeName); boolean add = false; if (info == null) { // create a new coverage store LOGGER.info("Auto-configuring coverage store: " + storeName); info = builder.buildCoverageStore(storeName); add = true; } else { // use the existing LOGGER.info("Using existing coverage store: " + storeName); } info.setType(coverageFormat.getName()); URL uploadedFileURL = DataUtilities.fileToURL(Resources.find(uploadedFile)); if (method.isInline()) { // TODO: create a method to figure out the relative url instead of making assumption // about the structure String defaultRoot = "/data/" + workspaceName + "/" + storeName; StringBuilder urlBuilder; try { urlBuilder = new StringBuilder( Resources.find(uploadedFile).toURI().toURL().toString()); } catch (MalformedURLException e) { throw new RestException("Error create building coverage URL", HttpStatus.INTERNAL_SERVER_ERROR, e); } String url; if (uploadedFile.getType() == Type.DIRECTORY && uploadedFile.name().equals(storeName)) { int def = urlBuilder.indexOf(defaultRoot); if (def >= 0) { url = "file:data/" + workspaceName + "/" + storeName; } else { url = urlBuilder.toString(); } } else { int def = urlBuilder.indexOf(defaultRoot); if (def >= 0) { String itemPath = urlBuilder.substring(def + defaultRoot.length()); url = "file:data/" + workspaceName + "/" + storeName + itemPath; } else { url = urlBuilder.toString(); } } if (url.contains("+")) { url = url.replace("+", "%2B"); } if (url.contains(" ")) { url = url.replace(" ", "%20"); } info.setURL(url); } else { info.setURL(uploadedFileURL.toExternalForm()); } // add or update the datastore info if (add) { if (!catalog.validate(info, true).isValid()) { throw new RuntimeException("Validation failed"); } catalog.add(info); } else { if (!catalog.validate(info, false).isValid()) { throw new RuntimeException("Validation failed"); } catalog.save(info); } builder.setStore(info); // check configure parameter, if set to none to not try to configure coverage if ("none".equalsIgnoreCase(configure)) { return null; } GridCoverage2DReader reader = null; try { reader = ((AbstractGridFormat) coverageFormat) .getReader(uploadedFileURL); if (reader == null) { throw new RestException("Could not aquire reader for coverage.", HttpStatus.INTERNAL_SERVER_ERROR); } // coverage read params final Map customParameters = new HashMap(); if (useJaiImageRead != null) { customParameters.put(AbstractGridFormat.USE_JAI_IMAGEREAD.getName().toString(), useJaiImageRead); } // check if the name of the coverage was specified String[] names = reader.getGridCoverageNames(); if (names.length > 1 && coverageName != null) { throw new RestException("The reader found more than one coverage, " + "coverageName cannot be used in this case (it would generate " + "the same name for all coverages found", HttpStatus.BAD_REQUEST); } // configure all available coverages, preserving backwards compatibility for the // case of single coverage reader if (names.length > 1) { for (String name : names) { SingleGridCoverage2DReader singleReader = new SingleGridCoverage2DReader(reader, name); configureCoverageInfo(builder, info, add, name, name, singleReader, customParameters); } } else { configureCoverageInfo(builder, info, add, names[0], coverageName, reader, customParameters); } // poach the coverage store data format return wrapObject(info, CoverageStoreInfo.class); } catch (RestException e) { throw e; } catch (Exception e) { throw new RestException("Error auto-configuring coverage", HttpStatus.INTERNAL_SERVER_ERROR, e); } finally { if (reader != null) { try { reader.dispose(); } catch (IOException e) { // it's ok, we tried } } } } private Format getCoverageFormat(String format) { String coverageFormatName = FORMAT_LOOKUP.get(format); if (coverageFormatName == null) { throw new RestException("Unsupported format: " + format + ", available formats are: " + FORMAT_LOOKUP.keySet().toString(), HttpStatus.BAD_REQUEST); } try { return CoverageStoreUtils.acquireFormat(coverageFormatName); } catch (Exception e) { throw new RestException("Coveragestore format unavailable: " + coverageFormatName, HttpStatus.INTERNAL_SERVER_ERROR); } } private void configureCoverageInfo(CatalogBuilder builder, CoverageStoreInfo storeInfo, boolean add, String nativeName, String coverageName, GridCoverage2DReader reader, final Map customParameters) throws Exception { CoverageInfo cinfo = builder.buildCoverage(reader, customParameters); if (coverageName != null) { cinfo.setName(coverageName); } if (nativeName != null) { cinfo.setNativeCoverageName(nativeName); } if (!add) { // update the existing String name = coverageName != null ? coverageName : nativeName; CoverageInfo existing = catalog.getCoverageByCoverageStore(storeInfo, name); if (existing == null) { // grab the first if there is only one List<CoverageInfo> coverages = catalog.getCoveragesByCoverageStore(storeInfo); // single coverage reader? if (coverages.size() == 1 && coverages.get(0).getNativeName() == null) { existing = coverages.get(0); } // check if we have it or not if (coverages.size() == 0) { // no coverages yet configured, change add flag and continue on add = true; } else { for (CoverageInfo ci : coverages) { if (ci.getNativeName().equals(name)) { existing = ci; } } if (existing == null) { add = true; } } } if (existing != null) { builder.updateCoverage(existing, cinfo); catalog.validate(existing, false).throwIfInvalid(); catalog.save(existing); cinfo = existing; } } // do some post configuration, if srs is not known or unset, transform to 4326 if ("UNKNOWN".equals(cinfo.getSRS())) { cinfo.setSRS("EPSG:4326"); } // add/save if (add) { catalog.validate(cinfo, true).throwIfInvalid(); catalog.add(cinfo); LayerInfo layerInfo = builder.buildLayer(cinfo); boolean valid = true; try { if (!catalog.validate(layerInfo, true).isValid()) { valid = false; } } catch (Exception e) { valid = false; } layerInfo.setEnabled(valid); catalog.add(layerInfo); } else { catalog.save(cinfo); } } @Override protected Resource findPrimaryFile(Resource directory, String format) { AbstractGridFormat coverageFormat = (AbstractGridFormat) getCoverageFormat(format); // first check if the format accepts a whole directory if (coverageFormat.accepts(directory.dir())) { return directory; } for (Resource f : directory.list()) { if (f.getType() == Type.DIRECTORY) { Resource result = findPrimaryFile(f, format); if (result != null) { return result; } } else { if (coverageFormat.accepts(f.file())) { return f; } } } return null; } /** * Does the file upload based on the specified method. * * @param method The method, one of 'file.' (inline), 'url.' (via url), or 'external.' (already on server) * @param storeName The name of the store being added * @param format The store format. * @throws IOException */ protected List<Resource> doFileUpload(UploadMethod method, String workspaceName, String storeName, String format, HttpServletRequest request) throws IOException { Resource directory = null; boolean postRequest = request != null && HttpMethod.POST.name().equalsIgnoreCase(request.getMethod()); // Prepare the directory only in case this is not an external upload if (method.isInline()) { // Mapping of the input directory if (method == UploadMethod.url) { // For URL upload method, workspace and StoreName are not considered directory = createFinalRoot(null, null, postRequest); } else { directory = createFinalRoot(workspaceName, storeName, postRequest); } } return handleFileUpload(storeName, workspaceName, method, format, directory, request); } private Resource createFinalRoot(String workspaceName, String storeName, boolean isPost) throws IOException { // Check if the Request is a POST request, in order to search for an existing coverage Resource directory = null; if (isPost && storeName != null) { // Check if the coverage already exists CoverageStoreInfo coverage = catalog.getCoverageStoreByName(storeName); if (coverage != null) { if (workspaceName == null || coverage.getWorkspace().getName().equalsIgnoreCase(workspaceName)) { // If the coverage exists then the associated directory is defined by its URL directory = Resources.fromPath( DataUtilities.urlToFile(new URL(coverage.getURL())).getPath(), catalog.getResourceLoader().get("")); } } } // If the directory has not been found then it is created directly if (directory == null) { directory = catalog.getResourceLoader() .get(Paths.path("data", workspaceName, storeName)); } // Selection of the original ROOT directory path StringBuilder root = new StringBuilder(directory.path()); // StoreParams to use for the mapping. Map<String, String> storeParams = new HashMap<>(); // Listing of the available pathMappers List<RESTUploadPathMapper> mappers = GeoServerExtensions .extensions(RESTUploadPathMapper.class); // Mapping of the root directory for (RESTUploadPathMapper mapper : mappers) { mapper.mapStorePath(root, workspaceName, storeName, storeParams); } directory = Resources.fromPath(root.toString()); return directory; } }