org.geoserver.rest.catalog.StyleController.java Source code

Java tutorial

Introduction

Here is the source code for org.geoserver.rest.catalog.StyleController.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.rest.catalog;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.logging.Logger;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.geoserver.catalog.CascadeDeleteVisitor;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CatalogBuilder;
import org.geoserver.catalog.CatalogFacade;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.ResourcePool;
import org.geoserver.catalog.SLDHandler;
import org.geoserver.catalog.StyleHandler;
import org.geoserver.catalog.StyleInfo;
import org.geoserver.catalog.Styles;
import org.geoserver.config.GeoServerDataDirectory;
import org.geoserver.platform.resource.Resource;
import org.geoserver.rest.PutIgnoringExtensionContentNegotiationStrategy;
import org.geoserver.rest.RestBaseController;
import org.geoserver.rest.util.IOUtils;
import org.geoserver.rest.ResourceNotFoundException;
import org.geoserver.rest.RestException;
import org.geoserver.rest.util.MediaTypeExtensions;
import org.geoserver.rest.wrapper.RestWrapper;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.styling.SLDParser;
import org.geotools.styling.Style;
import org.geotools.styling.StyledLayerDescriptor;
import org.geotools.util.Version;
import org.geotools.util.logging.Logging;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
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;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.xml.sax.EntityResolver;

import com.google.common.io.Files;

/**
 * Example style resource controller
 */
@RestController
@RequestMapping(path = RestBaseController.ROOT_PATH, produces = { MediaType.APPLICATION_JSON_VALUE,
        MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_HTML_VALUE })
public class StyleController extends AbstractCatalogController {

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

    @Autowired
    public StyleController(@Qualifier("catalog") Catalog catalog) {
        super(catalog);
    }

    @GetMapping(value = { "/styles", "/layers/{layerName}/styles", "/workspaces/{workspaceName}/styles" })
    public RestWrapper<?> stylesGet(@PathVariable(required = false) String layerName,
            @PathVariable(required = false) String workspaceName,
            @RequestParam(value = "quietOnNotFound", required = false) boolean quietOnNotFound) {

        if (workspaceName != null && catalog.getWorkspaceByName(workspaceName) == null) {
            throw new ResourceNotFoundException("Workspace " + workspaceName + " not found");
        }

        if (layerName != null) {
            return wrapList(catalog.getLayerByName(layerName).getStyles(), StyleInfo.class);
        } else if (workspaceName != null) {
            return wrapList(catalog.getStylesByWorkspace(workspaceName), StyleInfo.class);
        }
        List<StyleInfo> styles = catalog.getStylesByWorkspace(CatalogFacade.NO_WORKSPACE);
        return wrapList(styles, StyleInfo.class);
    }

    @PostMapping(value = { "/styles", "/layers/{layerName}/styles",
            "/workspaces/{workspaceName}/styles" }, consumes = { MediaType.TEXT_XML_VALUE,
                    MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE,
                    MediaTypeExtensions.TEXT_JSON_VALUE })
    @ResponseStatus(HttpStatus.CREATED)
    public String stylePost(@RequestBody StyleInfo style, @PathVariable(required = false) String layerName,
            @PathVariable(required = false) String workspaceName,
            @RequestParam(defaultValue = "false", name = "default") boolean makeDefault) {

        if (workspaceName != null && catalog.getWorkspaceByName(workspaceName) == null) {
            throw new ResourceNotFoundException("Workspace " + workspaceName + " not found");
        }
        checkFullAdminRequired(workspaceName);

        if (layerName != null) {
            StyleInfo existing = catalog.getStyleByName(style.getName());
            if (existing == null) {
                throw new ResourceNotFoundException();
            }

            LayerInfo l = catalog.getLayerByName(layerName);
            l.getStyles().add(existing);

            //check for default
            if (makeDefault) {
                l.setDefaultStyle(existing);
            }
            catalog.save(l);
            LOGGER.info("POST style " + style.getName() + " to layer " + layerName);
        } else {

            if (workspaceName != null) {
                style.setWorkspace(catalog.getWorkspaceByName(workspaceName));
            }

            catalog.add(style);
            LOGGER.info("POST style " + style.getName());
        }

        return style.getName();
    }

    @PostMapping(value = { "/styles", "/workspaces/{workspaceName}/styles" }, consumes = { SLDHandler.MIMETYPE_11,
            SLDHandler.MIMETYPE_10 })
    public ResponseEntity<String> styleSLDPost(@RequestBody Style style,
            @PathVariable(required = false) String workspaceName, @RequestParam(required = false) String name,
            @RequestHeader("Content-Type") String contentType, UriComponentsBuilder builder) {

        if (workspaceName != null && catalog.getWorkspaceByName(workspaceName) == null) {
            throw new ResourceNotFoundException("Workspace " + workspaceName + " not found");
        }
        checkFullAdminRequired(workspaceName);

        StyleHandler handler = org.geoserver.catalog.Styles.handler(contentType);
        if (name == null) {
            name = findNameFromObject(style);
        }

        //ensure that the style does not already exist
        if (catalog.getStyleByName(workspaceName, name) != null) {
            throw new RestException("Style " + name + " already exists.", HttpStatus.FORBIDDEN);
        }

        StyleInfo sinfo = catalog.getFactory().createStyle();
        sinfo.setName(name);
        sinfo.setFilename(name + "." + handler.getFileExtension());
        sinfo.setFormat(handler.getFormat());
        sinfo.setFormatVersion(handler.versionForMimeType(contentType));

        if (workspaceName != null) {
            sinfo.setWorkspace(catalog.getWorkspaceByName(workspaceName));
        }

        // ensure that a existing resource does not already exist, because we may not want to overwrite it
        GeoServerDataDirectory dataDir = new GeoServerDataDirectory(catalog.getResourceLoader());
        if (dataDir.style(sinfo).getType() != Resource.Type.UNDEFINED) {
            String msg = "Style resource " + sinfo.getFilename() + " already exists.";
            throw new RestException(msg, HttpStatus.FORBIDDEN);
        }

        ResourcePool resourcePool = catalog.getResourcePool();
        try {
            if (style instanceof Style) {
                resourcePool.writeStyle(sinfo, (Style) style);
            } else {
                resourcePool.writeStyle(sinfo, (InputStream) style);
            }
        } catch (IOException e) {
            throw new RestException("Error writing style", HttpStatus.INTERNAL_SERVER_ERROR, e);
        }

        catalog.add(sinfo);
        LOGGER.info("POST Style " + name);
        //build the new path
        UriComponents uriComponents = getUriComponents(name, workspaceName, builder);
        HttpHeaders headers = new HttpHeaders();
        headers.setLocation(uriComponents.toUri());
        return new ResponseEntity<String>(name, headers, HttpStatus.CREATED);
    }

    private UriComponents getUriComponents(String name, String workspace, UriComponentsBuilder builder) {
        UriComponents uriComponents;
        if (workspace != null) {
            uriComponents = builder.path("/workspaces/{workspaceName}/styles/{styleName}").buildAndExpand(workspace,
                    name);
        } else {
            uriComponents = builder.path("/styles/{id}").buildAndExpand(name);
        }
        return uriComponents;
    }

    @GetMapping(path = { "/styles/{styleName}", "/workspaces/{workspaceName}/styles/{styleName}" }, produces = {
            MediaType.ALL_VALUE })
    protected RestWrapper<StyleInfo> styleGet(@PathVariable String styleName,
            @PathVariable(required = false) String workspaceName) {

        return wrapObject(getStyleInternal(styleName, workspaceName), StyleInfo.class);
    }

    @GetMapping(path = { "/styles/{styleName}", "/workspaces/{workspaceName}/styles/{styleName}" }, produces = {
            SLDHandler.MIMETYPE_10, SLDHandler.MIMETYPE_11 })
    protected StyleInfo styleSLDGet(@PathVariable String styleName,
            @PathVariable(required = false) String workspaceName) {

        return getStyleInternal(styleName, workspaceName);
    }

    protected StyleInfo getStyleInternal(String styleName, String workspace) {
        LOGGER.fine("GET style " + styleName);
        StyleInfo sinfo = workspace == null ? catalog.getStyleByName(styleName)
                : catalog.getStyleByName(workspace, styleName);

        if (sinfo == null) {
            String message = "No such style: " + styleName;
            if (workspace != null) {
                message = "No such style " + styleName + " in workspace " + workspace;
            }
            throw new ResourceNotFoundException(message);
        } else {
            return sinfo;
        }
    }

    @DeleteMapping(path = { "/styles/{styleName}", "/workspaces/{workspaceName}/styles/{styleName}" })
    protected void styleDelete(@PathVariable String styleName, @PathVariable(required = false) String workspaceName,
            @RequestParam(required = false, defaultValue = "false") boolean recurse,
            @RequestParam(required = false, defaultValue = "false") boolean purge) throws IOException {

        if (workspaceName != null && catalog.getWorkspaceByName(workspaceName) == null) {
            throw new ResourceNotFoundException("Workspace " + workspaceName + " not found");
        }

        StyleInfo style = workspaceName != null ? catalog.getStyleByName(workspaceName, styleName)
                : catalog.getStyleByName(styleName);

        if (style == null) {
            throw new ResourceNotFoundException("Style " + styleName + " not found");
        }

        if (recurse) {
            new CascadeDeleteVisitor(catalog).visit(style);
        } else {
            // ensure that no layers reference the style
            List<LayerInfo> layers = catalog.getLayers(style);
            if (!layers.isEmpty()) {
                throw new RestException("Can't delete style referenced by existing layers.", HttpStatus.FORBIDDEN);
            }
            catalog.remove(style);
        }

        catalog.getResourcePool().deleteStyle(style, purge);

        LOGGER.info("DELETE style " + styleName);
    }

    String findNameFromObject(Object object) {
        String name = null;
        if (object instanceof Style) {
            name = ((Style) object).getName();
        }

        if (name == null) {
            // generate a random one
            for (int i = 0; name == null && i < 100; i++) {
                String candidate = "style-" + UUID.randomUUID().toString().substring(0, 7);
                if (catalog.getStyleByName(candidate) == null) {
                    name = candidate;
                }
            }
        }

        if (name == null) {
            throw new RestException("Unable to generate style name, specify one with 'name' " + "parameter",
                    HttpStatus.INTERNAL_SERVER_ERROR);
        }

        return name;
    }

    @PostMapping(value = { "/styles", "/workspaces/{workspaceName}/styles" }, consumes = {
            MediaTypeExtensions.APPLICATION_ZIP_VALUE })
    public ResponseEntity<String> stylePost(InputStream stream, @RequestParam(required = false) String name,
            @PathVariable(required = false) String workspaceName, UriComponentsBuilder builder) throws IOException {

        if (workspaceName != null && catalog.getWorkspaceByName(workspaceName) == null) {
            throw new ResourceNotFoundException("Workspace " + workspaceName + " not found");
        }
        checkFullAdminRequired(workspaceName);

        File directory = unzipSldPackage(stream);
        File uploadedFile = retrieveSldFile(directory);

        Style styleSld = parseSld(uploadedFile);

        if (name == null) {
            name = findNameFromObject(styleSld);
        }

        //ensure that the style does not already exist
        if (catalog.getStyleByName(workspaceName, name) != null) {
            throw new RestException("Style " + name + " already exists.", HttpStatus.FORBIDDEN);
        }

        // save image resources
        saveImageResources(directory, workspaceName);

        //create a style info object
        StyleInfo styleInfo = catalog.getFactory().createStyle();
        styleInfo.setName(name);
        styleInfo.setFilename(name + ".sld");

        if (workspaceName != null) {
            styleInfo.setWorkspace(catalog.getWorkspaceByName(workspaceName));
        }

        Resource style = dataDir.style(styleInfo);
        // ensure that a existing resource does not already exist, because we may not want to overwrite it
        if (dataDir.style(styleInfo).getType() != Resource.Type.UNDEFINED) {
            String msg = "Style resource " + styleInfo.getFilename() + " already exists.";
            throw new RestException(msg, HttpStatus.FORBIDDEN);
        }

        serializeSldFileInCatalog(style, uploadedFile);

        catalog.add(styleInfo);

        LOGGER.info("POST Style Package: " + name + ", workspace: " + workspaceName);
        UriComponents uriComponents = getUriComponents(name, workspaceName, builder);
        HttpHeaders headers = new HttpHeaders();
        headers.setLocation(uriComponents.toUri());
        return new ResponseEntity<>(name, headers, HttpStatus.CREATED);
    }

    @PutMapping(value = { "/styles/{styleName}", "/workspaces/{workspaceName}/styles/{styleName}" }, consumes = {
            MediaTypeExtensions.APPLICATION_ZIP_VALUE })
    public void styleZipPut(InputStream is, @PathVariable String styleName,
            @PathVariable(required = false) String workspaceName, @RequestParam(required = false) String name) {

        putZipInternal(is, workspaceName, name, styleName);
    }

    /**
     * Workaround to support regular response content type when extension is in path
     */
    @Configuration
    static class StyleControllerConfiguration {
        @Bean
        PutIgnoringExtensionContentNegotiationStrategy stylePutContentNegotiationStrategy() {
            return new PutIgnoringExtensionContentNegotiationStrategy(
                    new PatternsRequestCondition("/styles/{styleName}",
                            "/workspaces/{workspaceName}/styles/{styleName}"),
                    Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_HTML));
        }
    }

    @PutMapping(value = { "/styles/{styleName}", "/workspaces/{workspaceName}/styles/{styleName}" }, consumes = {
            MediaType.ALL_VALUE })
    public void stylePut(@PathVariable String styleName, @PathVariable(required = false) String workspaceName,
            @RequestParam(name = "raw", required = false, defaultValue = "false") boolean raw,
            HttpServletRequest request) throws IOException {

        if (workspaceName != null && catalog.getWorkspaceByName(workspaceName) == null) {
            throw new ResourceNotFoundException("Workspace " + workspaceName + " not found");
        }
        checkFullAdminRequired(workspaceName);
        StyleInfo s = catalog.getStyleByName(workspaceName, styleName);

        String contentType = request.getContentType();
        // String extentsion = "sld"; // TODO: determine this from path

        ResourcePool resourcePool = catalog.getResourcePool();
        if (raw) {
            writeRaw(s, request.getInputStream());
        } else {
            String content = IOUtils.toString(request.getInputStream());
            EntityResolver entityResolver = catalog.getResourcePool().getEntityResolver();
            for (StyleHandler format : Styles.handlers()) {
                for (Version version : format.getVersions()) {
                    String mimeType = format.mimeType(version);
                    if (!mimeType.equals(contentType)) {
                        continue; // skip this format
                    }
                    try {
                        StyledLayerDescriptor sld = format.parse(content, version, null, entityResolver);
                        Style style = Styles.style(sld);
                        if (format instanceof SLDHandler) {
                            s.setFormat(format.getFormat());
                            resourcePool.writeStyle(s, style, true);
                            catalog.save(s);
                        } else {
                            s.setFormat(format.getFormat());
                            writeRaw(s, request.getInputStream());
                        }
                        return;
                    } catch (Exception invalid) {
                        throw new RestException("Invalid style:" + invalid.getMessage(), HttpStatus.BAD_REQUEST,
                                invalid);
                    }
                }
            }
            throw new RestException("Unknown style fomrat '" + contentType + "'", HttpStatus.BAD_REQUEST);
        }
    }

    private void writeRaw(StyleInfo info, InputStream input) throws IOException {
        ResourcePool resourcePool = catalog.getResourcePool();

        resourcePool.writeStyle(info, input);
        catalog.save(info);
    }

    @PutMapping(value = { "/styles/{styleName}", "/workspaces/{workspaceName}/styles/{styleName}" }, consumes = {
            MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE,
            MediaTypeExtensions.TEXT_JSON_VALUE })
    public void stylePut(@RequestBody StyleInfo info, @PathVariable String styleName,
            @PathVariable(required = false) String workspaceName) {

        if (workspaceName != null && catalog.getWorkspaceByName(workspaceName) == null) {
            throw new ResourceNotFoundException("Workspace " + workspaceName + " not found");
        }
        checkFullAdminRequired(workspaceName);

        StyleInfo original = catalog.getStyleByName(workspaceName, styleName);

        //ensure no workspace change
        if (info.getWorkspace() != null) {
            if (!info.getWorkspace().equals(original.getWorkspace())) {
                throw new RestException("Can't change the workspace of a style, instead "
                        + "DELETE from existing workspace and POST to new workspace", HttpStatus.FORBIDDEN);
            }
        }

        new CatalogBuilder(catalog).updateStyle(original, info);
        catalog.save(original);
    }

    /**
     * Unzips the ZIP stream.
     *
     */
    private File unzipSldPackage(InputStream object) throws IOException {
        File tempDir = Files.createTempDir();

        org.geoserver.util.IOUtils.decompress(object, tempDir);

        return tempDir;
    }

    /**
     * Returns the sld file in the given directory. If no sld file, throws an exception
     *
     * @param directory
     *
     */
    private File retrieveSldFile(File directory) {
        File[] matchingFiles = directory.listFiles((dir, name) -> name.endsWith("sld"));

        if (matchingFiles.length == 0) {
            throw new RestException("No sld file provided:", HttpStatus.FORBIDDEN);
        }

        LOGGER.fine("retrieveSldFile (sldFile): " + matchingFiles[0].getAbsolutePath());

        return matchingFiles[0];
    }

    /**
     * Parses the sld file.
     *
     * @param sldFile
     *
     */
    private Style parseSld(File sldFile) {
        Style style = null;
        InputStream is = null;

        try {
            is = new FileInputStream(sldFile);

            SLDParser parser = new SLDParser(CommonFactoryFinder.getStyleFactory(null), is);
            EntityResolver resolver = catalog.getResourcePool().getEntityResolver();
            if (resolver != null) {
                parser.setEntityResolver(resolver);
            }

            Style[] styles = parser.readXML();
            if (styles.length > 0) {
                style = styles[0];
            }

            if (style == null) {
                throw new RestException("Style error.", HttpStatus.BAD_REQUEST);
            }

            return style;

        } catch (Exception ex) {
            LOGGER.severe(ex.getMessage());
            throw new RestException("Style error. " + ex.getMessage(), HttpStatus.BAD_REQUEST);

        } finally {
            IOUtils.closeQuietly(is);
        }
    }

    /**
     * Save the image resources in the styles folder
     *
     * @param directory     Temporary directory with images from SLD package
     * @param workspace     Geoserver workspace name for the style
     * @throws java.io.IOException
     */
    private void saveImageResources(File directory, String workspace) throws IOException {
        Resource stylesDir = workspace == null ? dataDir.getStyles()
                : dataDir.getStyles(catalog.getWorkspaceByName(workspace));

        File[] imageFiles = retrieveImageFiles(directory);

        for (int i = 0; i < imageFiles.length; i++) {
            IOUtils.copyStream(new FileInputStream(imageFiles[i]), stylesDir.get(imageFiles[i].getName()).out(),
                    true, true);
        }
    }

    /**
     * Returns a list of image files in the given directory
     *
     * @param directory
     *
     */
    private File[] retrieveImageFiles(File directory) {
        return directory.listFiles(
                (dir, name) -> validImageFileExtensions.contains(FilenameUtils.getExtension(name).toLowerCase()));
    }

    /**
     * Serializes the uploaded sld file in the catalog
     *
     * @param sldFile
     * @param uploadedSldFile
     */
    private void serializeSldFileInCatalog(Resource sldFile, File uploadedSldFile) {
        BufferedOutputStream out = null;
        try {
            out = new BufferedOutputStream(sldFile.out());
            byte[] sldContent = FileUtils.readFileToByteArray(uploadedSldFile);
            out.write(sldContent);
            out.flush();
        } catch (IOException e) {
            throw new RestException("Error creating file", HttpStatus.INTERNAL_SERVER_ERROR, e);
        } finally {
            IOUtils.closeQuietly(out);
        }
    }

    // TODO: This method is not called from anywhere? can it be removed
    private void putZipInternal(InputStream is, String workspace, String name, String style) {
        if (workspace != null && catalog.getWorkspaceByName(workspace) == null) {
            throw new ResourceNotFoundException("Workspace " + workspace + " not found");
        }
        checkFullAdminRequired(workspace);

        File directory = null;
        try {
            directory = unzipSldPackage(is);
            File uploadedFile = retrieveSldFile(directory);

            Style styleSld = parseSld(uploadedFile);

            if (name == null) {
                name = findNameFromObject(styleSld);
            }

            if (name == null) {
                throw new RestException("Style must have a name.", HttpStatus.BAD_REQUEST);
            }

            //ensure that the style does already exist
            if (!existsStyleInCatalog(workspace, name)) {
                throw new RestException("Style " + name + " doesn't exists.", HttpStatus.FORBIDDEN);
            }

            // save image resources
            saveImageResources(directory, workspace);

            // Save the style: serialize the style out into the data directory
            StyleInfo styleInfo = catalog.getStyleByName(workspace, style);
            serializeSldFileInCatalog(dataDir.style(styleInfo), uploadedFile);

            LOGGER.info("PUT Style Package: " + name + ", workspace: " + workspace);

        } catch (Exception e) {
            LOGGER.severe("Error processing the style package (PUT): " + e.getMessage());
            throw new RestException("Error processing the style", HttpStatus.INTERNAL_SERVER_ERROR, e);
        } finally {
            FileUtils.deleteQuietly(directory);
        }
    }

    /**
     * Checks if style is in the catalog.
     *
     * @param workspaceName     Workspace name
     * @param name              Style name
     */
    private boolean existsStyleInCatalog(String workspaceName, String name) {
        return (catalog.getStyleByName(workspaceName, name) != null);
    }
}