org.geoserver.importer.rest.ImportTaskController.java Source code

Java tutorial

Introduction

Here is the source code for org.geoserver.importer.rest.ImportTaskController.java

Source

/* (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.importer.rest;

import net.sf.json.JSONException;
import net.sf.json.JSONObject;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.IOUtils;
import org.geoserver.catalog.*;
import org.geoserver.catalog.impl.LayerInfoImpl;
import org.geoserver.catalog.impl.StoreInfoImpl;
import org.geoserver.importer.*;
import org.geoserver.importer.rest.converters.ImportJSONReader;
import org.geoserver.importer.rest.converters.ImportJSONWriter;
import org.geoserver.importer.transform.TransformChain;
import org.geoserver.rest.PutIgnoringExtensionContentNegotiationStrategy;
import org.geoserver.rest.RequestInfo;
import org.geoserver.rest.RestBaseController;
import org.geoserver.rest.RestException;
import org.geoserver.rest.util.MediaTypeExtensions;
import org.geotools.referencing.CRS;
import org.geotools.util.logging.Logging;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

@RestController
@RequestMapping(path = RestBaseController.ROOT_PATH + "/imports/{id}/tasks", produces = {
        MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_HTML_VALUE })
public class ImportTaskController extends ImportBaseController {

    static final Logger LOGGER = Logging.getLogger(ImportTaskController.class);

    @Autowired
    protected ImportTaskController(Importer importer) {
        super(importer);
    }

    @GetMapping
    public ImportWrapper tasksGet(@PathVariable Long id, @RequestParam(required = false) String expand) {
        return (writer, builder, converter) -> converter.tasks(builder, context(id).getTasks(), true,
                converter.expand(expand, 0));
    }

    @GetMapping(path = "/{taskId}")
    public ImportTask taskGet(@PathVariable Long id, @PathVariable Integer taskId) {
        return task(id, taskId, false);
    }

    @GetMapping(path = "/{taskId}/progress")
    public ImportWrapper progressGet(@PathVariable Long id, @PathVariable Integer taskId) {

        JSONObject progress = new JSONObject();
        ImportTask inProgress = importer.getCurrentlyProcessingTask(id);
        try {
            if (inProgress != null) {
                progress.put("progress", inProgress.getNumberProcessed());
                progress.put("total", inProgress.getTotalToProcess());
                progress.put("state", inProgress.getState().toString());
            } else {
                ImportTask task = task(id, taskId);
                progress.put("state", task.getState().toString());
                if (task.getState() == ImportTask.State.ERROR) {
                    if (task.getError() != null) {
                        progress.put("message", task.getError().getMessage());
                    }
                }
            }
        } catch (JSONException jex) {
            throw new RestException("Internal Error", HttpStatus.INTERNAL_SERVER_ERROR, jex);
        }
        return (writer, builder, converter) -> writer.write(progress.toString());
    }

    @GetMapping(path = "/{taskId}/target")
    public ImportWrapper targetGet(@PathVariable Long id, @PathVariable Integer taskId,
            @RequestParam(required = false) String expand) {
        final ImportTask task = task(id, taskId);
        if (task.getStore() == null) {
            throw new RestException("Task has no target store", HttpStatus.NOT_FOUND);
        }
        return (writer, builder, converter) -> converter.store(builder, task.getStore(), task, true,
                converter.expand(expand, 1));

    }

    @GetMapping(path = "/{taskId}/layer")
    public ImportWrapper layersGet(@PathVariable Long id, @PathVariable Integer taskId,
            @RequestParam(required = false) String expand) {
        ImportTask task = task(id, taskId);
        return (writer, builder, converter) -> converter.layer(builder, task, true, converter.expand(expand, 1));
    }

    @PostMapping(consumes = { MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_FORM_URLENCODED_VALUE })
    public Object taskPost(@PathVariable Long id, @RequestParam(required = false) String expand,
            HttpServletRequest request, HttpServletResponse response) {
        ImportData data = null;

        LOGGER.info("Handling POST of " + request.getContentType());
        //file posted from form
        MediaType mimeType = MediaType.valueOf(request.getContentType());
        if (request.getContentType().startsWith(MediaType.MULTIPART_FORM_DATA_VALUE)) {
            data = handleMultiPartFormUpload(request, context(id));
        } else if (request.getContentType().startsWith(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
            try {
                data = handleFormPost(request);
            } catch (IOException | ServletException e) {
                throw new RestException(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR, e);
            }
        }

        if (data == null) {
            throw new RestException("Unsupported POST", HttpStatus.FORBIDDEN);
        }

        //Construct response
        return acceptData(data, context(id), response, expand);
    }

    @PutMapping(path = "/{taskId}", consumes = { MediaType.APPLICATION_JSON_VALUE,
            MediaTypeExtensions.TEXT_JSON_VALUE })
    public ImportWrapper taskPut(@PathVariable Long id, @PathVariable Integer taskId, HttpServletRequest request,
            HttpServletResponse response) {

        return (writer, builder, converter) -> handleTaskPut(id, taskId, request, response, converter);
    }

    /**
     * Workaround to support regular response content type when file extension is in path
     */
    @Configuration
    static class ImportTaskControllerConfiguration {
        @Bean
        PutIgnoringExtensionContentNegotiationStrategy importTaskPutContentNegotiationStrategy() {
            return new PutIgnoringExtensionContentNegotiationStrategy(
                    new PatternsRequestCondition("/imports/{id}/tasks/{taskId:.+}"),
                    Arrays.asList(MediaType.APPLICATION_JSON, MediaType.TEXT_HTML));
        }
    }

    @PutMapping(path = "/{taskId:.+}")
    public Object taskPutFile(@PathVariable Long id, @PathVariable Object taskId,
            @RequestParam(required = false) String expand, HttpServletRequest request,
            HttpServletResponse response) {

        ImportContext context = context(id);
        //TODO: Task id is the file name here. This functionality is completely undocumented
        return acceptData(handleFileUpload(context, taskId, request), context, response, expand);
    }

    @PutMapping(path = "/{taskId}/target")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void targetPut(@PathVariable Long id, @PathVariable Integer taskId, @RequestBody StoreInfo store) {

        if (store == null) {
            throw new RestException("Task has no target store", HttpStatus.NOT_FOUND);
        } else {
            updateStoreInfo(task(id, taskId), store, importer);
            importer.changed(task(id, taskId));
        }
    }

    @PutMapping(path = "/{taskId}/layer")
    @ResponseStatus(HttpStatus.ACCEPTED)
    public ImportWrapper layerPut(@PathVariable Long id, @PathVariable Integer taskId,
            @RequestParam(required = false) String expand, @RequestBody LayerInfo layer) {
        ImportTask task = task(id, taskId);

        return (writer, builder, converter) -> {
            updateLayer(task, layer, importer, converter);
            importer.changed(task);
            converter.task(builder, task, true, converter.expand(expand, 1));
        };
    }

    @DeleteMapping(path = "/{taskId}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void taskDelete(@PathVariable Long id, @PathVariable Integer taskId) {
        ImportTask task = task(id, taskId);
        task.getContext().removeTask(task);
        importer.changed(task.getContext());
    }

    public Object acceptData(ImportData data, ImportContext context, HttpServletResponse response, String expand) {
        List<ImportTask> newTasks = null;
        try {
            newTasks = importer.update(context, data);
        } catch (ValidationException ve) {
            throw ImportJSONWriter.badRequest(ve.getMessage());
        } catch (IOException e) {
            throw new RestException("Error updating context", HttpStatus.INTERNAL_SERVER_ERROR, e);
        }
        if (!newTasks.isEmpty()) {
            final List<ImportTask> result = newTasks;
            if (newTasks.size() == 1) {
                long taskId = newTasks.get(0).getId();
                response.setHeader("Location", RequestInfo.get()
                        .servletURI(String.format("/imports/%d/tasks/%d", context.getId(), taskId)));
            }
            response.setStatus(HttpStatus.CREATED.value());

            return (ImportWrapper) (writer, builder, converter) -> {
                if (result.size() == 1) {
                    converter.task(builder, result.get(0), true, converter.expand(expand, 1));
                } else {
                    converter.tasks(builder, result, true, converter.expand(expand, 0));
                }
            };
        }
        return "";
    }

    public ImportData handleFormPost(HttpServletRequest request) throws IOException, ServletException {

        String url = IOUtils.toString(request.getPart("url").getInputStream(), encoding);
        if (url == null) {
            throw new RestException("Invalid request", HttpStatus.BAD_REQUEST);
        }
        URL location = null;
        try {
            location = new URL(url);
        } catch (MalformedURLException ex) {
            LOGGER.warning("invalid URL specified in upload : " + url);
        }
        // @todo handling remote URL implies asynchronous processing at this stage
        if (location == null || !location.getProtocol().equalsIgnoreCase("file")) {
            throw new RestException("Invalid url in request", HttpStatus.BAD_REQUEST);
        }
        FileData file;
        try {
            file = FileData.createFromFile(new File(location.toURI().getPath()));
        } catch (Exception ex) {
            throw new RuntimeException("Unexpected exception", ex);
        }

        if (file instanceof Directory) {
            try {
                file.prepare();
            } catch (IOException ioe) {
                String msg = "Error processing file: " + file.getFile().getAbsolutePath();
                LOGGER.log(Level.WARNING, msg, ioe);
                throw new RestException(msg, HttpStatus.INTERNAL_SERVER_ERROR);
            }
        }

        return file;
    }

    public ImportData handleMultiPartFormUpload(HttpServletRequest request, ImportContext context) {
        DiskFileItemFactory factory = new DiskFileItemFactory();
        // @revisit - this appears to be causing OOME
        //factory.setSizeThreshold(102400000);

        ServletFileUpload upload = new ServletFileUpload(factory);
        List<FileItem> items = null;
        try {
            items = upload.parseRequest(request);
        } catch (FileUploadException e) {
            throw new RestException("File upload failed", HttpStatus.INTERNAL_SERVER_ERROR, e);
        }

        //look for a directory to hold the files
        Directory directory = findOrCreateDirectory(context);

        //unpack all the files
        for (FileItem item : items) {
            if (item.getName() == null) {
                continue;
            }
            try {
                directory.accept(item);
            } catch (Exception ex) {
                throw new RestException("Error writing file " + item.getName(), HttpStatus.INTERNAL_SERVER_ERROR,
                        ex);
            }
        }
        return directory;
    }

    void handleTaskPut(Long id, Integer taskId, HttpServletRequest request, HttpServletResponse response,
            ImportJSONWriter converter) {
        ImportTask orig = task(id, taskId);
        ImportTask task;
        try {
            ImportJSONReader reader = new ImportJSONReader(importer);
            task = reader.task(request.getInputStream());
            //            task = new ImportContextJSONConverterReader(importer,request.getInputStream()).task();
        } catch (ValidationException | IOException ve) {
            LOGGER.log(Level.WARNING, null, ve);
            throw converter.badRequest(ve.getMessage());
        }

        boolean change = false;
        if (task.getStore() != null) {
            //JD: moved to TaskTargetResource, but handle here for backward compatability
            updateStoreInfo(orig, task.getStore(), importer);
            change = true;
        }
        if (task.getData() != null) {
            //TODO: move this to data endpoint
            orig.getData().setCharsetEncoding(task.getData().getCharsetEncoding());
            change = true;
        }
        if (task.getUpdateMode() != null) {
            orig.setUpdateMode(task.getUpdateMode());
            change = orig.getUpdateMode() != task.getUpdateMode();
        }

        if (task.getLayer() != null) {
            change = true;
            //now handled by LayerResource, but handle here for backwards compatability
            updateLayer(orig, task.getLayer(), importer, converter);
        }

        TransformChain chain = task.getTransform();
        if (chain != null) {
            orig.setTransform(chain);
            change = true;
        }

        if (!change) {
            throw new RestException("Unknown representation", HttpStatus.BAD_REQUEST);
        } else {
            importer.changed(orig);
            response.setStatus(HttpStatus.NO_CONTENT.value());
        }
    }

    private Directory findOrCreateDirectory(ImportContext context) {
        if (context.getData() instanceof Directory) {
            return (Directory) context.getData();
        }

        try {
            return Directory.createNew(importer.getUploadRoot());
        } catch (IOException ioe) {
            throw new RestException("File upload failed", HttpStatus.INTERNAL_SERVER_ERROR, ioe);
        }
    }

    private ImportData handleFileUpload(ImportContext context, Object taskId, HttpServletRequest request) {
        Directory directory = findOrCreateDirectory(context);
        try {
            directory.accept(taskId.toString(), request.getInputStream());
        } catch (IOException e) {
            throw new RestException("Error unpacking file", HttpStatus.INTERNAL_SERVER_ERROR, e);
        }

        return directory;
    }

    static void updateLayer(ImportTask orig, LayerInfo l, Importer importer, ImportJSONWriter converter) {
        //update the original layer and resource from the new

        ResourceInfo r = l.getResource();

        //TODO: this is not thread safe, clone the object before overwriting it
        //save the existing resource, which will be overwritten below,
        ResourceInfo resource = orig.getLayer().getResource();

        if (r != null) {
            // we support the following resource info properties:
            // (don't just use blindly copy everything)
            if (r.getTitle() != null) {
                resource.setTitle(r.getTitle());
            }
            if (r.getAbstract() != null) {
                resource.setAbstract(r.getAbstract());
            }
            if (r.getDescription() != null) {
                resource.setDescription(r.getDescription());
            }
        }

        CatalogBuilder cb = new CatalogBuilder(importer.getCatalog());
        l.setResource(resource);
        // @hack workaround OWSUtils bug - trying to copy null collections
        // why these are null in the first place is a different question
        LayerInfoImpl impl = (LayerInfoImpl) orig.getLayer();
        if (impl.getAuthorityURLs() == null) {
            impl.setAuthorityURLs(new ArrayList(1));
        }
        if (impl.getIdentifiers() == null) {
            impl.setIdentifiers(new ArrayList(1));
        }
        // @endhack
        cb.updateLayer(orig.getLayer(), l);

        // validate SRS - an invalid one will destroy capabilities doc and make
        // the layer totally broken in UI
        CoordinateReferenceSystem newRefSystem = null;

        String srs = r != null ? r.getSRS() : null;
        if (srs != null) {
            try {
                newRefSystem = CRS.decode(srs);
            } catch (NoSuchAuthorityCodeException ex) {
                String msg = "Invalid SRS " + srs;
                LOGGER.warning(msg + " in PUT request");
                throw converter.badRequest(msg);
            } catch (FactoryException ex) {
                throw new RestException("Error with referencing", HttpStatus.INTERNAL_SERVER_ERROR, ex);
            }
            // make this the specified native if none exists
            // useful for csv or other files
            if (resource.getNativeCRS() == null) {
                resource.setNativeCRS(newRefSystem);
            }
            resource.setSRS(srs);
        }

    }

    static void updateStoreInfo(ImportTask task, StoreInfo update, Importer importer) {
        //handle three cases here:
        // 1. current task store is null -> set the update as the new store
        // 2. update is reference to an existing store -> set the update as the new store
        // 3. update is a partial change to the current store -> update the current

        // allow an existing store to be referenced as the target

        StoreInfo orig = task.getStore();

        //check if the update is referencing an existing store
        StoreInfo existing = null;
        if (update.getName() != null) {
            Catalog cat = importer.getCatalog();
            if (update.getWorkspace() != null) {
                existing = cat.getStoreByName(update.getWorkspace(), update.getName(), StoreInfo.class);
            } else {
                existing = importer.getCatalog().getStoreByName(update.getName(), StoreInfo.class);
            }
            if (existing == null) {
                throw new RestException("Unable to find referenced store", HttpStatus.BAD_REQUEST);
            }
            if (!existing.isEnabled()) {
                throw new RestException("Proposed target store is not enabled", HttpStatus.BAD_REQUEST);
            }
        }

        if (existing != null) {
            //JD: not sure why we do this, rather than just task.setStore(existing);
            CatalogBuilder cb = new CatalogBuilder(importer.getCatalog());

            StoreInfo clone;
            if (existing instanceof DataStoreInfo) {
                clone = cb.buildDataStore(existing.getName());
                cb.updateDataStore((DataStoreInfo) clone, (DataStoreInfo) existing);
            } else if (existing instanceof CoverageStoreInfo) {
                clone = cb.buildCoverageStore(existing.getName());
                cb.updateCoverageStore((CoverageStoreInfo) clone, (CoverageStoreInfo) existing);
            } else {
                throw new RestException("Unable to handle existing store: " + update,
                        HttpStatus.INTERNAL_SERVER_ERROR);
            }

            ((StoreInfoImpl) clone).setId(existing.getId());
            task.setStore(clone);
            task.setDirect(false);
        } else if (orig == null) {
            task.setStore(update);
        } else {
            //update the original
            CatalogBuilder cb = new CatalogBuilder(importer.getCatalog());
            if (orig instanceof DataStoreInfo) {
                cb.updateDataStore((DataStoreInfo) orig, (DataStoreInfo) update);
            } else if (orig instanceof CoverageStoreInfo) {
                cb.updateCoverageStore((CoverageStoreInfo) orig, (CoverageStoreInfo) update);
            } else {
                throw new RestException("Unable to update store with " + update, HttpStatus.INTERNAL_SERVER_ERROR);
            }
        }
    }
}