org.geoserver.opensearch.rest.CollectionsController.java Source code

Java tutorial

Introduction

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

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Pattern;
import java.util.stream.IntStream;

import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.FileImageInputStream;
import javax.media.jai.ImageLayout;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.geoserver.catalog.CascadeDeleteVisitor;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CatalogBuilder;
import org.geoserver.catalog.CoverageInfo;
import org.geoserver.catalog.CoverageStoreInfo;
import org.geoserver.catalog.CoverageView;
import org.geoserver.catalog.CoverageView.CompositionType;
import org.geoserver.catalog.CoverageView.CoverageBand;
import org.geoserver.catalog.CoverageView.InputCoverageBand;
import org.geoserver.catalog.DataStoreInfo;
import org.geoserver.catalog.DimensionDefaultValueSetting;
import org.geoserver.catalog.DimensionDefaultValueSetting.Strategy;
import org.geoserver.catalog.DimensionInfo;
import org.geoserver.catalog.DimensionPresentation;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.catalog.StoreInfo;
import org.geoserver.catalog.StyleInfo;
import org.geoserver.catalog.impl.DimensionInfoImpl;
import org.geoserver.opensearch.eo.OpenSearchAccessProvider;
import org.geoserver.opensearch.eo.store.CollectionLayer;
import org.geoserver.opensearch.eo.store.OpenSearchAccess;
import org.geoserver.ows.URLMangler.URLType;
import org.geoserver.ows.util.ResponseUtils;
import org.geoserver.platform.GeoServerResourceLoader;
import org.geoserver.platform.resource.Resource;
import org.geoserver.platform.resource.Resource.Type;
import org.geoserver.rest.ResourceNotFoundException;
import org.geoserver.rest.RestBaseController;
import org.geoserver.rest.RestException;
import org.geoserver.rest.util.MediaTypeExtensions;
import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader;
import org.geotools.coverage.grid.io.AbstractGridFormat;
import org.geotools.coverage.grid.io.GridFormatFinder;
import org.geotools.data.DataUtilities;
import org.geotools.data.FeatureSource;
import org.geotools.data.FeatureStore;
import org.geotools.data.Query;
import org.geotools.data.collection.ListFeatureCollection;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.factory.Hints;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.NameImpl;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.gce.imagemosaic.ImageMosaicFormat;
import org.geotools.gce.imagemosaic.Utils;
import org.geotools.image.io.ImageIOExt;
import org.geotools.referencing.CRS;
import org.geotools.styling.Style;
import org.geotools.styling.builder.ChannelSelectionBuilder;
import org.geotools.styling.builder.RasterSymbolizerBuilder;
import org.geotools.util.Version;
import org.opengis.feature.Feature;
import org.opengis.feature.Property;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.Name;
import org.opengis.feature.type.PropertyDescriptor;
import org.opengis.filter.Filter;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.sort.SortBy;
import org.opengis.filter.sort.SortOrder;
import org.opengis.referencing.FactoryException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * Controller for the OpenSearch collections
 *
 * @author Andrea Aime - GeoSolutions
 */
@RestController
@ControllerAdvice
@RequestMapping(path = RestBaseController.ROOT_PATH + "/oseo/collections")
public class CollectionsController extends AbstractOpenSearchController {

    private static final String TIME_START = "timeStart";

    static final Hints EXCLUDE_MOSAIC_HINTS = new Hints(Utils.EXCLUDE_MOSAIC, true);

    /**
     * List of parts making up a zipfile for a collection
     */
    enum CollectionPart implements ZipPart {
        Collection("collection.json"), Description("description.html"), Metadata("metadata.xml"), Thumbnail(
                "thumbnail\\.[png|jpeg|jpg]"), OwsLinks("owsLinks.json");

        Pattern pattern;

        CollectionPart(String pattern) {
            this.pattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
        }

        @Override
        public boolean matches(String name) {
            return pattern.matcher(name).matches();
        }
    }

    @FunctionalInterface
    public interface IOConsumer<T> {
        void accept(T t) throws IOException;
    }

    static final List<String> COLLECTION_HREFS = Arrays.asList("ogcLinksHref", "metadataHref", "descriptionHref",
            "thumbnailHref");

    static final Name COLLECTION_ID = new NameImpl(OpenSearchAccess.EO_NAMESPACE, "identifier");

    Catalog catalog;

    public CollectionsController(OpenSearchAccessProvider accessProvider, OseoJSONConverter jsonConverter,
            Catalog catalog) {
        super(accessProvider, jsonConverter);
        this.catalog = catalog;
    }

    @GetMapping(produces = { MediaType.APPLICATION_JSON_VALUE })
    @ResponseBody
    public CollectionReferences getCollections(HttpServletRequest request,
            @RequestParam(name = "offset", required = false) Integer offset,
            @RequestParam(name = "limit", required = false) Integer limit) throws IOException {
        // query the collections for their names
        Query query = new Query();
        setupQueryPaging(query, offset, limit);
        query.setSortBy(new SortBy[] { FF.sort("name", SortOrder.ASCENDING) });
        query.setPropertyNames(new String[] { "name" });
        OpenSearchAccess access = accessProvider.getOpenSearchAccess();
        FeatureStore<FeatureType, Feature> fs = (FeatureStore<FeatureType, Feature>) access.getCollectionSource();
        FeatureCollection<FeatureType, Feature> features = fs.getFeatures(query);

        // map to java beans for JSON encoding
        String baseURL = ResponseUtils.baseURL(request);
        List<CollectionReference> list = new ArrayList<>();
        features.accepts(f -> {
            String name = (String) f.getProperty("name").getValue();
            String collectionHref = ResponseUtils.buildURL(baseURL, "/rest/oseo/collections/" + name, null,
                    URLType.RESOURCE);
            String oseoHref = ResponseUtils.buildURL(baseURL, "/oseo/description",
                    Collections.singletonMap("parentId", name), URLType.RESOURCE);
            CollectionReference cr = new CollectionReference(name, collectionHref, oseoHref);
            list.add(cr);
        }, null);
        return new CollectionReferences(list);
    }

    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> postCollectionJson(HttpServletRequest request,
            @RequestBody(required = true) SimpleFeature feature) throws IOException, URISyntaxException {
        String eoId = checkCollectionIdentifier(feature);
        Feature collectionFeature = simpleToComplex(feature, getCollectionSchema(), COLLECTION_HREFS);

        // insert the new feature
        runTransactionOnCollectionStore(fs -> fs.addFeatures(singleton(collectionFeature)));

        // if got here, all is fine
        return returnCreatedCollectionReference(request, eoId);
    }

    private ResponseEntity<String> returnCreatedCollectionReference(HttpServletRequest request, String eoId)
            throws URISyntaxException {
        String baseURL = ResponseUtils.baseURL(request);
        String newCollectionLocation = ResponseUtils.buildURL(baseURL, "/rest/oseo/collections/" + eoId, null,
                URLType.RESOURCE);
        HttpHeaders headers = new HttpHeaders();
        headers.setLocation(new URI(newCollectionLocation));
        return new ResponseEntity<>(eoId, headers, HttpStatus.CREATED);
    }

    @PostMapping(consumes = MediaTypeExtensions.APPLICATION_ZIP_VALUE)
    public ResponseEntity<String> postCollectionZip(HttpServletRequest request, InputStream body)
            throws IOException, URISyntaxException {

        Map<CollectionPart, byte[]> parts = parsePartsFromZip(body, CollectionPart.values());

        // process the collection part
        final byte[] collectionPayload = parts.get(CollectionPart.Collection);
        if (collectionPayload == null) {
            throw new RestException("collection.json file is missing from the zip", HttpStatus.BAD_REQUEST);
        }
        SimpleFeature jsonFeature = parseGeoJSONFeature("collection.json", collectionPayload);
        String eoId = checkCollectionIdentifier(jsonFeature);
        Feature collectionFeature = simpleToComplex(jsonFeature, getCollectionSchema(), COLLECTION_HREFS);

        // grab the other parts
        byte[] description = parts.get(CollectionPart.Description);
        byte[] metadata = parts.get(CollectionPart.Metadata);
        byte[] rawLinks = parts.get(CollectionPart.OwsLinks);
        SimpleFeatureCollection linksCollection;
        if (rawLinks != null) {
            OgcLinks links = parseJSON(OgcLinks.class, rawLinks);
            linksCollection = beansToLinksCollection(links);
        } else {
            linksCollection = null;
        }

        // insert the new feature and accessory bits
        runTransactionOnCollectionStore(fs -> {
            fs.addFeatures(singleton(collectionFeature));

            final String nsURI = fs.getSchema().getName().getNamespaceURI();
            Filter filter = FF.equal(FF.property(COLLECTION_ID), FF.literal(eoId), true);

            if (description != null) {
                String descriptionString = new String(description);
                fs.modifyFeatures(new NameImpl(nsURI, OpenSearchAccess.DESCRIPTION), descriptionString, filter);
            }

            if (metadata != null) {
                String descriptionString = new String(metadata);
                fs.modifyFeatures(OpenSearchAccess.METADATA_PROPERTY_NAME, descriptionString, filter);
            }

            if (linksCollection != null) {
                fs.modifyFeatures(OpenSearchAccess.OGC_LINKS_PROPERTY_NAME, linksCollection, filter);
            }

        });

        return returnCreatedCollectionReference(request, eoId);
    }

    @GetMapping(path = "{collection}", produces = { MediaType.APPLICATION_JSON_VALUE })
    @ResponseBody
    public SimpleFeature getCollection(HttpServletRequest request,
            @PathVariable(name = "collection", required = true) String collection) throws IOException {
        // grab the collection
        Feature feature = queryCollection(collection, q -> {
        });

        // map to the output schema for GeoJSON encoding
        SimpleFeatureType targetSchema = mapFeatureTypeToSimple(feature.getType(), ftb -> {
            COLLECTION_HREFS.forEach(href -> ftb.add(href, String.class));
        });
        return mapFeatureToSimple(feature, targetSchema, fb -> {
            String baseURL = ResponseUtils.baseURL(request);
            String pathBase = "/rest/oseo/collections/" + collection + "/";
            String ogcLinks = ResponseUtils.buildURL(baseURL, pathBase + "ogcLinks", null, URLType.RESOURCE);
            String metadata = ResponseUtils.buildURL(baseURL, pathBase + "metadata", null, URLType.RESOURCE);
            String description = ResponseUtils.buildURL(baseURL, pathBase + "description", null, URLType.RESOURCE);
            String thumb = ResponseUtils.buildURL(baseURL, pathBase + "thumbnail", null, URLType.RESOURCE);

            fb.set("ogcLinksHref", ogcLinks);
            fb.set("metadataHref", metadata);
            fb.set("descriptionHref", description);
            fb.set("thumbnailHref", thumb);
        });
    }

    @PutMapping(path = "{collection}", consumes = MediaType.APPLICATION_JSON_VALUE)
    public void putCollectionJson(HttpServletRequest request,
            @PathVariable(required = true, name = "collection") String collection,
            @RequestBody(required = true) SimpleFeature feature) throws IOException, URISyntaxException {
        // check the collection exists
        queryCollection(collection, q -> {
        });

        // check the id, mind, could be different from the collection one if the client
        // is trying to change
        checkCollectionIdentifier(feature);

        // prepare the update, need to convert each field into a Name/Value couple
        Feature collectionFeature = simpleToComplex(feature, getCollectionSchema(), COLLECTION_HREFS);
        List<Name> names = new ArrayList<>();
        List<Object> values = new ArrayList<>();
        for (Property p : collectionFeature.getProperties()) {
            // skip over the large/complex attributes that are being modified via separate calls
            final Name propertyName = p.getName();
            if (OpenSearchAccess.METADATA_PROPERTY_NAME.equals(propertyName)
                    || OpenSearchAccess.OGC_LINKS_PROPERTY_NAME.equals(propertyName)
                    || OpenSearchAccess.DESCRIPTION.equals(propertyName.getLocalPart())) {
                continue;
            }
            names.add(propertyName);
            values.add(p.getValue());
        }
        Name[] attributeNames = (Name[]) names.toArray(new Name[names.size()]);
        Object[] attributeValues = (Object[]) values.toArray();
        Filter filter = FF.equal(FF.property(COLLECTION_ID), FF.literal(collection), true);
        runTransactionOnCollectionStore(fs -> fs.modifyFeatures(attributeNames, attributeValues, filter));
    }

    @DeleteMapping(path = "{collection}")
    public void deleteCollection(@PathVariable(required = true, name = "collection") String collection)
            throws IOException {
        // check the collection exists
        queryCollection(collection, q -> {
        });

        // TODO: handle cascading on products, and removing the publishing side without removing the metadata
        Filter filter = FF.equal(FF.property(COLLECTION_ID), FF.literal(collection), true);
        runTransactionOnCollectionStore(fs -> fs.removeFeatures(filter));
    }

    @GetMapping(path = "{collection}/layer", produces = { MediaType.APPLICATION_JSON_VALUE })
    @ResponseBody
    public CollectionLayer getCollectionLayer(HttpServletRequest request,
            @PathVariable(name = "collection", required = true) String collection) throws IOException {
        // query one collection and grab its OGC links
        final Name layerPropertyName = getCollectionLayerPropertyName();
        final PropertyName layerProperty = FF.property(layerPropertyName);
        Feature feature = queryCollection(collection, q -> {
            q.setProperties(Collections.singletonList(layerProperty));
        });

        CollectionLayer layer = buildCollectionLayerFromFeature(feature, true);
        return layer;
    }

    private Name getCollectionLayerPropertyName() throws IOException {
        String namespaceURI = getOpenSearchAccess().getCollectionSource().getSchema().getName().getNamespaceURI();
        final Name layerPropertyName = new NameImpl(namespaceURI, OpenSearchAccess.LAYER);
        return layerPropertyName;
    }

    @PutMapping(path = "{collection}/layer", consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Object> putCollectionLayer(
            @PathVariable(name = "collection", required = true) String collection,
            @RequestBody CollectionLayer layer) throws IOException {
        // check the collection is there
        queryCollection(collection, q -> {
        });

        // validate the layer
        validateLayer(layer);

        // check if the layer was there
        CollectionLayer previousLayer = getCollectionLayer(collection);

        // prepare the update
        SimpleFeature layerCollectionFeature = beanToLayerCollectionFeature(layer);
        Filter filter = FF.equal(FF.property(COLLECTION_ID), FF.literal(collection), true);
        final Name layerPropertyName = getCollectionLayerPropertyName();
        runTransactionOnCollectionStore(fs -> {
            fs.modifyFeatures(layerPropertyName, layerCollectionFeature, filter);
        });

        // this is done after the changes in the DB to make sure it's reading the same data
        // we are inserting (feature sources do not accept a transaction...)
        if (previousLayer != null) {
            removeMosaicAndLayer(previousLayer);
        }
        try {
            createMosaicAndLayer(collection, layer);
        } catch (Exception e) {
            // rethrow to make the transaction fail
            throw new RuntimeException(e);
        }

        // see if we have to return a creation or not
        if (previousLayer != null) {
            return new ResponseEntity<>(HttpStatus.OK);
        } else {
            return new ResponseEntity<>(HttpStatus.CREATED);
        }
    }

    private CollectionLayer getCollectionLayer(String collection) throws IOException {
        final Name layerPropertyName = getCollectionLayerPropertyName();
        final PropertyName layerProperty = FF.property(layerPropertyName);
        Feature feature = queryCollection(collection, q -> {
            q.setProperties(Collections.singletonList(layerProperty));
        });
        CollectionLayer layer = buildCollectionLayerFromFeature(feature, false);
        return layer;
    }

    private CollectionLayer buildCollectionLayerFromFeature(Feature feature, boolean notFoundOnEmpty) {
        CollectionLayer layer = CollectionLayer.buildCollectionLayerFromFeature(feature);
        if (layer == null && notFoundOnEmpty) {
            throw new ResourceNotFoundException();
        }

        return layer;
    }

    private void removeMosaicAndLayer(CollectionLayer previousLayer) throws IOException {
        // look for the layer and trace your way back to the store
        String name = previousLayer.getWorkspace() + ":" + previousLayer.getLayer();
        LayerInfo layerInfo = catalog.getLayerByName(name);
        if (layerInfo == null) {
            LOGGER.warning("Could not locate previous layer " + name
                    + ", skipping removal of old publishing configuration");
            return;
        }
        CascadeDeleteVisitor visitor = new CascadeDeleteVisitor(catalog);

        // see if it has the style the code normally attaches
        StyleInfo si = layerInfo.getDefaultStyle();
        if (si.getWorkspace() != null && previousLayer.getWorkspace().equals(si.getWorkspace().getName())
                && previousLayer.getLayer().equals(si.getName())) {
            visitor.visit(si);
        }

        // move to the resource
        ResourceInfo resourceInfo = layerInfo.getResource();
        if (resourceInfo == null) {
            LOGGER.warning("Located layer " + name
                    + ", but it references a dangling resource, cannot perform full removal, limiting"
                    + " to layer removal");
            visitor.visit(layerInfo);
            return;
        } else if (!(resourceInfo instanceof CoverageInfo)) {
            throw new RuntimeException(
                    "Unexpected, the old layer in configuration, " + name + ", is not a coverage, bailing out");
        }
        // see if we have a store
        StoreInfo storeInfo = resourceInfo.getStore();
        if (storeInfo == null) {
            LOGGER.warning("Located layer " + name
                    + ", but it references a dangling store , cannot perform full removal, limiting"
                    + " to layer and resource removal");
            visitor.visit((CoverageInfo) resourceInfo);
            return;
        }

        // cascade delete
        visitor.visit((CoverageStoreInfo) storeInfo);

        // and remove the configuration files too
        GeoServerResourceLoader rl = catalog.getResourceLoader();
        final String relativePath = rl.get("data") + "/" + previousLayer.getWorkspace() + "/"
                + previousLayer.getLayer();
        Resource mosaicResource = rl.fromPath(relativePath);
        mosaicResource.delete();
    }

    /**
     * Validates the layer and throws appropriate exceptions in case mandatory bits are missing
     * 
     * @param layer
     * @param catalog2
     */
    private void validateLayer(CollectionLayer layer) {
        if (layer.getWorkspace() == null) {
            throw new RestException("Invalid layer configuration, workspace name is missing or null",
                    HttpStatus.BAD_REQUEST);
        }
        if (catalog.getWorkspaceByName(layer.getWorkspace()) == null) {
            throw new RestException(
                    "Invalid layer configuration, workspace '" + layer.getWorkspace() + "' does not exist",
                    HttpStatus.BAD_REQUEST);
        }
        if (layer.getLayer() == null) {
            throw new RestException("Invalid layer configuration, layer name is missing or null",
                    HttpStatus.BAD_REQUEST);
        }

        if (layer.isSeparateBands()) {
            if (layer.getBands() == null || layer.getBands().length == 0) {
                throw new RestException("Invalid layer configuration, claims to have separate bands but does "
                        + "not list the band names", HttpStatus.BAD_REQUEST);
            }
            if (layer.getBrowseBands() == null || layer.getBrowseBands().length == 0) {
                throw new RestException(
                        "Invalid layer configuration, claims to have separate bands but does not "
                                + "list the browse band names (hence cannot setup a style for it)",
                        HttpStatus.BAD_REQUEST);
            } else if ((layer.getBrowseBands().length != 3 && layer.getBrowseBands().length != 1)) {
                throw new RestException(
                        "Invalid layer configuration, browse bands must be either " + "one (gray) or three (RGB)",
                        HttpStatus.BAD_REQUEST);
            } else if (layer.getBands().length > 0 && layer.getBrowseBands().length > 0
                    && !containedFully(layer.getBrowseBands(), layer.getBands())) {
                throw new RestException("Invalid layer configuration, browse bands contains entries "
                        + "that are not part of the bands declaration", HttpStatus.BAD_REQUEST);
            }
        }
        if (layer.isHeterogeneousCRS()) {
            if (layer.getMosaicCRS() == null) {
                throw new RestException(
                        "Invalid layer configuration, mosaic is heterogeneous but the mosaic CRS is missing",
                        HttpStatus.BAD_REQUEST);
            }
        }
        if (layer.getMosaicCRS() != null) {
            try {
                CRS.decode(layer.getMosaicCRS());
            } catch (FactoryException e) {
                throw new RestException("Invalid mosaicCRS value, cannot be decoded: " + e.getMessage(),
                        HttpStatus.BAD_REQUEST);
            }
        }

    }

    private boolean containedFully(int[] browseBands, int[] bands) {
        for (int i = 0; i < browseBands.length; i++) {
            int bb = browseBands[i];
            boolean found = false;
            for (int j = 0; j < bands.length; j++) {
                if (bands[j] == bb) {
                    found = true;
                    break;
                }
            }

            if (!found) {
                return false;
            }
        }
        return true;
    }

    private void createMosaicAndLayer(String collection, CollectionLayer layer) throws Exception {
        GeoServerResourceLoader rl = catalog.getResourceLoader();

        // grab the target directory and create it if needed
        final String relativePath = "data/" + layer.getWorkspace() + "/" + layer.getLayer();
        Resource mosaicDirectory = rl.fromPath(relativePath);
        if (mosaicDirectory.getType() != Type.UNDEFINED) {
            mosaicDirectory.dir();
        }

        if (layer.isSeparateBands()) {
            configureSeparateBandsMosaic(collection, layer, relativePath, mosaicDirectory);
        } else {
            configureSimpleMosaic(collection, layer, relativePath, mosaicDirectory);
        }
    }

    private void configureSeparateBandsMosaic(String collection, CollectionLayer layerConfiguration,
            String relativePath, Resource mosaicDirectory) throws Exception {
        // get the namespace URI for the store
        final FeatureSource<FeatureType, Feature> collectionSource = getOpenSearchAccess().getCollectionSource();
        final FeatureType schema = collectionSource.getSchema();
        final String nsURI = schema.getName().getNamespaceURI();

        // image mosaic won't automatically create the mosaic config for us in this case,
        // we have to setup both the mosaic property file and sample image for all bands
        for (int band : layerConfiguration.getBands()) {
            final String mosaicName = collection + OpenSearchAccess.BAND_LAYER_SEPARATOR + band;

            // get the sample granule
            File file = getSampleGranule(collection, nsURI, band, mosaicName);
            AbstractGridFormat format = (AbstractGridFormat) GridFormatFinder.findFormat(file,
                    EXCLUDE_MOSAIC_HINTS);
            if (format == null) {
                throw new RestException(
                        "Could not find a coverage reader able to process " + file.getAbsolutePath(),
                        HttpStatus.PRECONDITION_FAILED);
            }
            ImageLayout imageLayout;
            double[] nativeResolution;
            AbstractGridCoverage2DReader reader = null;
            try {
                reader = format.getReader(file);
                if (reader == null) {
                    throw new RestException(
                            "Could not find a coverage reader able to process " + file.getAbsolutePath(),
                            HttpStatus.PRECONDITION_FAILED);
                }
                imageLayout = reader.getImageLayout();
                nativeResolution = reader.getResolutionLevels()[0];
            } finally {
                if (reader != null) {
                    reader.dispose();
                }
            }
            ImageReaderSpi spi = null;
            try (FileImageInputStream fis = new FileImageInputStream(file)) {
                ImageReader imageReader = ImageIOExt.getImageioReader(fis);
                if (imageReader != null) {
                    spi = imageReader.getOriginatingProvider();
                }
            }

            // the mosaic configuration
            Properties mosaicConfig = new Properties();
            mosaicConfig.put("Levels", nativeResolution[0] + "," + nativeResolution[1]);
            mosaicConfig.put("Heterogeneous", "true");
            mosaicConfig.put("AbsolutePath", "true");
            mosaicConfig.put("Name", "" + band);
            mosaicConfig.put("TypeName", mosaicName);
            mosaicConfig.put("TypeNames", "false"); // disable typename scanning
            mosaicConfig.put("Caching", "false");
            mosaicConfig.put("LocationAttribute", "location");
            mosaicConfig.put("TimeAttribute", TIME_START);
            mosaicConfig.put("CanBeEmpty", "true");
            if (spi != null) {
                mosaicConfig.put("SuggestedSPI", spi.getClass().getName());
            }
            // TODO: the index is now always in 4326, so the mosaic has to be heterogeneous
            // in general, unless we know the data is uniformly in something else, in that
            // case we could reproject the view reporting the footprints...
            //            if (layerConfiguration.isHeterogeneousCRS()) {
            mosaicConfig.put("HeterogeneousCRS", "true");
            mosaicConfig.put("MosaicCRS", "EPSG:4326" /* layerConfiguration.getMosaicCRS() */);
            mosaicConfig.put("CrsAttribute", "crs");
            //            }
            Resource propertyResource = mosaicDirectory.get(band + ".properties");
            try (OutputStream os = propertyResource.out()) {
                mosaicConfig.store(os,
                        "DataStore configuration for collection '" + collection + "' and band '" + band + "'");
            }

            // create the sample image
            Resource sampleImageResource = mosaicDirectory.get(band + Utils.SAMPLE_IMAGE_NAME);
            Utils.storeSampleImage(sampleImageResource.file(), imageLayout.getSampleModel(null),
                    imageLayout.getColorModel(null));
        }

        // this is ridicolous, but for the moment, multi-crs mosaics won't work if there
        // is no indexer.properties around, even if no collection is actually done
        buildIndexer(collection, mosaicDirectory);

        // mosaic datastore connection
        createDataStoreProperties(collection, mosaicDirectory);

        // the mosaic datastore itself
        CatalogBuilder cb = new CatalogBuilder(catalog);
        CoverageStoreInfo mosaicStoreInfo = createMosaicStore(cb, collection, layerConfiguration, relativePath);

        // and finally the layer, with a coverage view associated to it
        List<CoverageBand> coverageBands = buildCoverageBands(layerConfiguration);
        final String coverageName = layerConfiguration.getLayer();
        final CoverageView coverageView = new CoverageView(coverageName, coverageBands);
        CoverageInfo coverageInfo = coverageView.createCoverageInfo(coverageName, mosaicStoreInfo, cb);
        timeEnableResource(coverageInfo);
        final LayerInfo layerInfo = cb.buildLayer(coverageInfo);

        catalog.add(coverageInfo);
        catalog.add(layerInfo);

        // configure the style if needed
        createStyle(layerConfiguration, layerInfo);
    }

    private List<CoverageBand> buildCoverageBands(CollectionLayer collectionLayer) {
        List<CoverageBand> result = new ArrayList<>();
        int[] bands = collectionLayer.getBands();
        for (int i = 0; i < bands.length; i++) {
            int band = bands[i];
            CoverageBand cb = new CoverageBand(Collections.singletonList(new InputCoverageBand("" + band, "0")),
                    "B" + i, i, CompositionType.BAND_SELECT);
            result.add(cb);
        }
        return result;
    }

    private File getSampleGranule(String collection, final String nsURI, int band, final String mosaicName)
            throws IOException {
        // make sure there is at least one granule to grab resolution, sample/color model,
        // and preferred SPI
        SimpleFeatureSource granuleSource = DataUtilities
                .simple(getOpenSearchAccess().getFeatureSource(new NameImpl(nsURI, mosaicName)));
        SimpleFeature firstFeature = DataUtilities.first(granuleSource.getFeatures());
        if (firstFeature == null) {
            throw new RestException(
                    "Could not locate any granule for collection '" + collection + "' and band '" + band + "'",
                    HttpStatus.EXPECTATION_FAILED);
        }
        // grab the file
        String location = (String) firstFeature.getAttribute("location");
        File file = new File(location);
        if (!file.exists()) {
            throw new RestException(
                    "Sample granule '" + location + "' could not be found on the file system, check your database",
                    HttpStatus.EXPECTATION_FAILED);
        }

        return file;
    }

    private void configureSimpleMosaic(String collection, CollectionLayer layerConfiguration,
            final String relativePath, Resource mosaic) throws IOException, Exception {
        // make sure there is at least one granule
        final FeatureSource<FeatureType, Feature> collectionSource = getOpenSearchAccess().getCollectionSource();
        final FeatureType schema = collectionSource.getSchema();
        final String nsURI = schema.getName().getNamespaceURI();
        final NameImpl fsName = new NameImpl(nsURI, collection);
        final FeatureSource<FeatureType, Feature> genericGranuleSource = getOpenSearchAccess()
                .getFeatureSource(fsName);
        SimpleFeatureSource granuleSource = DataUtilities.simple(genericGranuleSource);
        SimpleFeature firstFeature = DataUtilities.first(granuleSource.getFeatures());
        if (firstFeature == null) {
            throw new RestException("Cannot configure a mosaic, please add at least one product "
                    + "with granules in order to set it up", HttpStatus.PRECONDITION_FAILED);
        }

        buildIndexer(collection, mosaic);

        createDataStoreProperties(collection, mosaic);

        CatalogBuilder cb = new CatalogBuilder(catalog);
        createMosaicStore(cb, collection, layerConfiguration, relativePath);

        // and then the layer
        CoverageInfo coverageInfo = cb.buildCoverage(collection);
        coverageInfo.setName(layerConfiguration.getLayer());
        timeEnableResource(coverageInfo);
        catalog.add(coverageInfo);
        LayerInfo layerInfo = cb.buildLayer(coverageInfo);
        catalog.add(layerInfo);

        // configure the style if needed
        createStyle(layerConfiguration, layerInfo);
    }

    private void buildIndexer(String collection, Resource mosaic) throws IOException {
        // prepare the mosaic configuration
        Properties indexer = new Properties();
        indexer.put("UseExistingSchema", "true");
        indexer.put("Name", collection);
        indexer.put("TypeName", collection);
        indexer.put("AbsolutePath", "true");
        indexer.put("TimeAttribute", TIME_START);
        // TODO: should we setup also a end time and prepare a interval based time setup?

        // TODO: the index is now always in 4326, so the mosaic has to be heterogeneous
        // in general, unless we know the data is uniformly in something else, in that
        // case we could reproject the view reporting the footprints...
        // if (layerConfiguration.isHeterogeneousCRS()) {
        indexer.put("HeterogeneousCRS", "true");
        indexer.put("MosaicCRS", "EPSG:4326" /* layerConfiguration.getMosaicCRS() */);
        indexer.put("CrsAttribute", "crs");
        // }
        Resource resource = mosaic.get("indexer.properties");
        try (OutputStream os = resource.out()) {
            indexer.store(os, "Indexer for collection: " + collection);
        }
    }

    /**
     * Enables the ResourceInfo time dimension, defaulting it to the highest available value 
     * TODO: it's probably useful to make this configurable 
     */
    private void timeEnableResource(ResourceInfo resource) {
        DimensionInfo dimension = new DimensionInfoImpl();
        dimension.setEnabled(true);
        dimension.setAttribute(TIME_START);
        dimension.setUnits(DimensionInfo.TIME_UNITS);
        dimension.setPresentation(DimensionPresentation.CONTINUOUS_INTERVAL);
        DimensionDefaultValueSetting defaultValueSetting = new DimensionDefaultValueSetting();
        defaultValueSetting.setStrategyType(Strategy.MAXIMUM);
        dimension.setDefaultValue(defaultValueSetting);

        resource.getMetadata().put(ResourceInfo.TIME, dimension);
    }

    private void createStyle(CollectionLayer layerConfiguration, LayerInfo layerInfo) throws IOException {
        CoverageInfo ci = (CoverageInfo) layerInfo.getResource();
        int[] defaultBands = IntStream.rangeClosed(1, ci.getDimensions().size()).toArray();
        final int[] browseBands = layerConfiguration.getBrowseBands();
        if (browseBands != null && browseBands.length > 0 && !Arrays.equals(defaultBands, browseBands)) {
            RasterSymbolizerBuilder rsb = new RasterSymbolizerBuilder();
            if (browseBands.length == 1) {
                ChannelSelectionBuilder cs = rsb.channelSelection();
                cs.gray().channelName("" + browseBands[0]);
            } else if (browseBands.length == 3) {
                ChannelSelectionBuilder cs = rsb.channelSelection();
                cs.red().channelName("" + browseBands[0]);
                cs.green().channelName("" + browseBands[1]);
                cs.blue().channelName("" + browseBands[2]);
            }
            Style style = rsb.buildStyle();
            StyleInfo si = catalog.getFactory().createStyle();
            si.setFormat("SLD");
            si.setFormatVersion(new Version("1.0"));
            si.setName(layerInfo.getName());
            si.setWorkspace(catalog.getWorkspaceByName(layerConfiguration.getWorkspace()));
            si.setFilename(layerInfo.getName() + ".sld");
            catalog.getResourcePool().writeStyle(si, style);
            catalog.add(si);

            // associate style (we need a proxy instance, cannot use the original layerinfo)
            LayerInfo savedLayer = catalog.getLayer(layerInfo.getId());
            savedLayer.setDefaultStyle(si);
            catalog.save(savedLayer);
        }

    }

    private void createDataStoreProperties(String collection, Resource mosaic) throws IOException {
        // prepare the datastore.properties now
        // TODO : should we use the store identifier instead of the prefixed name? Would be
        // resilient across store renames
        Properties datastore = new Properties();
        datastore.put("StoreName", prefixedName(getOpenSearchStoreInfo()));
        Resource datastoreResource = mosaic.get("datastore.properties");
        try (OutputStream os = datastoreResource.out()) {
            datastore.store(os, "DataStore configuration for collection: " + collection);
        }
    }

    private CoverageStoreInfo createMosaicStore(CatalogBuilder cb, String collection, CollectionLayer layer,
            final String relativePath) {
        // good to go, create the store
        cb.setWorkspace(catalog.getWorkspace(layer.getWorkspace()));
        CoverageStoreInfo mosaicStore = cb.buildCoverageStore(layer.getLayer());
        mosaicStore.setType(new ImageMosaicFormat().getName());
        mosaicStore.setDescription("Image mosaic wrapping OpenSearch collection: " + collection);
        mosaicStore.setURL("file:/" + relativePath);
        catalog.add(mosaicStore);
        cb.setStore(mosaicStore);

        return mosaicStore;
    }

    private Object prefixedName(DataStoreInfo info) {
        String ws = info.getWorkspace().getName();
        String name = info.getName();
        return ws + ":" + name;
    }

    @DeleteMapping(path = "{collection}/layer")
    public void deleteCollectionLayer(@PathVariable(name = "collection", required = true) String collection)
            throws IOException {
        // check the collection is there
        queryCollection(collection, q -> {
        });

        // prepare the update
        Filter filter = FF.equal(FF.property(COLLECTION_ID), FF.literal(collection), true);
        final Name layerPropertyName = getCollectionLayerPropertyName();
        runTransactionOnCollectionStore(fs -> {
            CollectionLayer previousLayer = getCollectionLayer(collection);

            // remove from DB
            fs.modifyFeatures(layerPropertyName, null, filter);

            // remove from configuration if needed
            if (previousLayer != null) {
                removeMosaicAndLayer(previousLayer);
            }
        });
    }

    private SimpleFeature beanToLayerCollectionFeature(CollectionLayer layer) throws IOException {
        Name collectionLayerPropertyName = getCollectionLayerPropertyName();
        PropertyDescriptor pd = getOpenSearchAccess().getCollectionSource().getSchema()
                .getDescriptor(collectionLayerPropertyName);
        SimpleFeatureType schema = (SimpleFeatureType) pd.getType();
        SimpleFeatureBuilder fb = new SimpleFeatureBuilder(schema);
        fb.set("workspace", layer.getWorkspace());
        fb.set("layer", layer.getLayer());
        fb.set("separateBands", layer.isSeparateBands());
        fb.set("bands", layer.getBands());
        fb.set("browseBands", layer.getBrowseBands());
        fb.set("heterogeneousCRS", layer.isHeterogeneousCRS());
        fb.set("mosaicCRS", layer.getMosaicCRS());
        SimpleFeature sf = fb.buildFeature(null);
        return sf;
    }

    @GetMapping(path = "{collection}/ogcLinks", produces = { MediaType.APPLICATION_JSON_VALUE })
    @ResponseBody
    public OgcLinks getCollectionOgcLinks(HttpServletRequest request,
            @PathVariable(name = "collection", required = true) String collection) throws IOException {
        // query one collection and grab its OGC links
        Feature feature = queryCollection(collection, q -> {
            q.setProperties(Collections.singletonList(FF.property(OpenSearchAccess.OGC_LINKS_PROPERTY_NAME)));
        });

        OgcLinks links = buildOgcLinksFromFeature(feature, true);
        return links;
    }

    @PutMapping(path = "{collection}/ogcLinks", consumes = MediaType.APPLICATION_JSON_VALUE)
    public void putCollectionOgcLinks(HttpServletRequest request,
            @PathVariable(name = "collection", required = true) String collection, @RequestBody OgcLinks links)
            throws IOException {
        // check the collection is there
        queryCollection(collection, q -> {
        });

        ListFeatureCollection linksCollection = beansToLinksCollection(links);

        Filter filter = FF.equal(FF.property(COLLECTION_ID), FF.literal(collection), true);
        runTransactionOnCollectionStore(
                fs -> fs.modifyFeatures(OpenSearchAccess.OGC_LINKS_PROPERTY_NAME, linksCollection, filter));
    }

    @DeleteMapping(path = "{collection}/ogcLinks")
    public void deleteCollectionLinks(@PathVariable(name = "collection", required = true) String collection)
            throws IOException {
        // check the collection is there
        queryCollection(collection, q -> {
        });

        // prepare the update
        Filter filter = FF.equal(FF.property(COLLECTION_ID), FF.literal(collection), true);
        runTransactionOnCollectionStore(
                fs -> fs.modifyFeatures(OpenSearchAccess.OGC_LINKS_PROPERTY_NAME, null, filter));
    }

    @GetMapping(path = "{collection}/metadata", produces = { MediaType.TEXT_XML_VALUE })
    public void getCollectionMetadata(@PathVariable(name = "collection", required = true) String collection,
            HttpServletResponse response) throws IOException {
        // query one collection and grab its OGC links
        Feature feature = queryCollection(collection, q -> {
            q.setProperties(Collections.singletonList(FF.property(OpenSearchAccess.METADATA_PROPERTY_NAME)));
        });

        // grab the metadata
        Property metadataProperty = feature.getProperty(OpenSearchAccess.METADATA_PROPERTY_NAME);
        if (metadataProperty != null && metadataProperty.getValue() instanceof String) {
            String value = (String) metadataProperty.getValue();
            response.setContentType("text/xml");
            StreamUtils.copy(value, Charset.forName("UTF-8"), response.getOutputStream());
        } else {
            throw new ResourceNotFoundException("Metadata for collection '" + collection + "' could not be found");
        }
    }

    @PutMapping(path = "{collection}/metadata", consumes = MediaType.TEXT_XML_VALUE)
    public void putCollectionMetadata(@PathVariable(name = "collection", required = true) String collection,
            HttpServletRequest request) throws IOException {
        // check the collection is there
        queryCollection(collection, q -> {
        });

        // TODO: validate it's actual ISO metadata
        String metadata = IOUtils.toString(request.getReader());
        checkWellFormedXML(metadata);

        // prepare the update
        Filter filter = FF.equal(FF.property(COLLECTION_ID), FF.literal(collection), true);
        runTransactionOnCollectionStore(
                fs -> fs.modifyFeatures(OpenSearchAccess.METADATA_PROPERTY_NAME, metadata, filter));
    }

    @DeleteMapping(path = "{collection}/metadata")
    public void deleteCollectionMetadata(@PathVariable(name = "collection", required = true) String collection)
            throws IOException {
        // check the collection is there
        queryCollection(collection, q -> {
        });

        // prepare the update
        Filter filter = FF.equal(FF.property(COLLECTION_ID), FF.literal(collection), true);
        runTransactionOnCollectionStore(
                fs -> fs.modifyFeatures(OpenSearchAccess.METADATA_PROPERTY_NAME, null, filter));
    }

    @GetMapping(path = "{collection}/description", produces = { MediaType.TEXT_HTML_VALUE })
    public void getCollectionDescription(@PathVariable(name = "collection", required = true) String collection,
            HttpServletResponse response) throws IOException {
        // query one collection and grab its OGC links
        Feature feature = queryCollection(collection, q -> {
            q.setPropertyNames(new String[] { OpenSearchAccess.DESCRIPTION });
        });

        // grab the description
        Property descriptionProperty = feature.getProperty(OpenSearchAccess.DESCRIPTION);
        if (descriptionProperty != null && descriptionProperty.getValue() instanceof String) {
            String value = (String) descriptionProperty.getValue();
            response.setContentType("text/html");
            StreamUtils.copy(value, Charset.forName("UTF-8"), response.getOutputStream());
        } else {
            throw new ResourceNotFoundException(
                    "Description for collection '" + collection + "' could not be found");
        }
    }

    @PutMapping(path = "{collection}/description", consumes = MediaType.TEXT_HTML_VALUE)
    public void putCollectionDescription(@PathVariable(name = "collection", required = true) String collection,
            HttpServletRequest request) throws IOException {
        // check the collection is there
        queryCollection(collection, q -> {
        });

        String description = IOUtils.toString(request.getReader());

        updateDescription(collection, description);
    }

    @DeleteMapping(path = "{collection}/description")
    public void deleteCollectionDescritiopn(@PathVariable(name = "collection", required = true) String collection)
            throws IOException {
        // check the collection is there
        queryCollection(collection, q -> {
        });

        // set it to null
        updateDescription(collection, null);
    }

    private void updateDescription(String collection, String description) throws IOException {
        // prepare the update
        Filter filter = FF.equal(FF.property(COLLECTION_ID), FF.literal(collection), true);
        runTransactionOnCollectionStore(fs -> {
            // set the description to null
            final FeatureSource<FeatureType, Feature> collectionSource = getOpenSearchAccess()
                    .getCollectionSource();
            final FeatureType schema = collectionSource.getSchema();
            final String nsURI = schema.getName().getNamespaceURI();
            fs.modifyFeatures(new NameImpl(nsURI, OpenSearchAccess.DESCRIPTION), description, filter);
        });
    }

    private void runTransactionOnCollectionStore(IOConsumer<FeatureStore> featureStoreConsumer) throws IOException {
        FeatureStore store = (FeatureStore) getOpenSearchAccess().getCollectionSource();
        super.runTransactionOnStore(store, featureStoreConsumer);
    }

    private String checkCollectionIdentifier(SimpleFeature feature) {
        // get the identifier and name, make sure they are the same
        String name = (String) feature.getAttribute("name");
        if (name == null) {
            throw new RestException("Missing mandatory property 'name'", HttpStatus.BAD_REQUEST);
        }
        String eoId = (String) feature.getAttribute("eo:identifier");
        if (eoId == null) {
            throw new RestException("Missing mandatory 'eo:identifier'", HttpStatus.BAD_REQUEST);
        }
        if (!eoId.equals(name)) {
            throw new RestException(
                    "Inconsistent, collection 'name' and 'eo:identifier' should have the same value (eventually name will be removed)",
                    HttpStatus.BAD_REQUEST);
        }
        return eoId;
    }

    FeatureType getCollectionSchema() throws IOException {
        final OpenSearchAccess access = accessProvider.getOpenSearchAccess();
        final FeatureSource<FeatureType, Feature> collectionSource = access.getCollectionSource();
        final FeatureType schema = collectionSource.getSchema();
        return schema;
    }

}