/* (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); } }