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

Java tutorial

Introduction

Here is the source code for org.geoserver.opensearch.rest.AbstractOpenSearchController.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.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.function.Consumer;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.io.IOUtils;
import org.geoserver.catalog.DataStoreInfo;
import org.geoserver.opensearch.eo.ListComplexFeatureCollection;
import org.geoserver.opensearch.eo.OpenSearchAccessProvider;
import org.geoserver.opensearch.eo.response.LinkFeatureComparator;
import org.geoserver.opensearch.eo.store.OpenSearchAccess;
import org.geoserver.opensearch.eo.store.OpenSearchAccess.ProductClass;
import org.geoserver.opensearch.rest.CollectionsController.IOConsumer;
import org.geoserver.rest.ResourceNotFoundException;
import org.geoserver.rest.RestBaseController;
import org.geoserver.rest.RestException;
import org.geotools.data.DataUtilities;
import org.geotools.data.DefaultTransaction;
import org.geotools.data.FeatureSource;
import org.geotools.data.FeatureStore;
import org.geotools.data.Query;
import org.geotools.data.Transaction;
import org.geotools.data.collection.ListFeatureCollection;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.feature.AttributeBuilder;
import org.geotools.feature.ComplexFeatureBuilder;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.feature.NameImpl;
import org.geotools.feature.collection.BaseSimpleFeatureCollection;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geojson.feature.FeatureJSON;
import org.geotools.util.logging.Logging;
import org.opengis.feature.Attribute;
import org.opengis.feature.Feature;
import org.opengis.feature.FeatureFactory;
import org.opengis.feature.Property;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.Name;
import org.opengis.feature.type.PropertyDescriptor;
import org.opengis.filter.FilterFactory2;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpStatus;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Base class for OpenSearch related REST controllers
 *
 * @author Andrea Aime - GeoSolutions
 */
public abstract class AbstractOpenSearchController extends RestBaseController {

    interface ZipPart {

        /**
         * Returns true if the part matches the provided name
         * @param name
         * @return
         */
        public boolean matches(String name);
    }

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

    static final String SOURCE_NAME = "SourceName";

    static final FeatureFactory FEATURE_FACTORY = CommonFactoryFinder.getFeatureFactory(null);

    static final FilterFactory2 FF = CommonFactoryFinder.getFilterFactory2();

    protected OpenSearchAccessProvider accessProvider;

    protected OseoJSONConverter jsonConverter;

    public AbstractOpenSearchController(OpenSearchAccessProvider accessProvider, OseoJSONConverter jsonConverter) {
        this.accessProvider = accessProvider;
        this.jsonConverter = jsonConverter;
    }

    protected OpenSearchAccess getOpenSearchAccess() throws IOException {
        return accessProvider.getOpenSearchAccess();
    }

    protected DataStoreInfo getOpenSearchStoreInfo() throws IOException {
        return accessProvider.getDataStoreInfo();
    }

    protected void validateMin(Integer value, int min, String name) {
        if (value != null && value < min) {
            throw new RestException("Invalid parameter " + name + ", should be at least " + min,
                    HttpStatus.BAD_REQUEST);
        }
    }

    protected void validateMax(Integer value, int max, String name) {
        if (value != null && value > max) {
            throw new RestException("Invalid parameter " + name + ", should be at most " + max,
                    HttpStatus.BAD_REQUEST);
        }
    }

    protected void setupQueryPaging(Query query, Integer offset, Integer limit) {
        if (offset != null) {
            validateMin(offset, 0, "offset");
            query.setStartIndex(offset);
        }
        final int maximumRecordsPerPage = accessProvider.getService().getMaximumRecordsPerPage();
        if (limit != null) {
            validateMin(limit, 0, "limit");
            validateMax(limit, maximumRecordsPerPage, "limit");
            query.setMaxFeatures(limit);
        } else {
            query.setMaxFeatures(maximumRecordsPerPage);
        }
    }

    protected FeatureCollection<FeatureType, Feature> queryCollections(Query query) throws IOException {
        OpenSearchAccess access = accessProvider.getOpenSearchAccess();
        FeatureSource<FeatureType, Feature> fs = access.getCollectionSource();
        FeatureCollection<FeatureType, Feature> fc = fs.getFeatures(query);
        return fc;
    }

    protected Feature queryCollection(String collectionName, Consumer<Query> queryDecorator) throws IOException {
        Query query = new Query();
        query.setFilter(FF.equal(FF.property("name"), FF.literal(collectionName), true));
        queryDecorator.accept(query);
        FeatureCollection<FeatureType, Feature> fc = queryCollections(query);
        Feature feature = DataUtilities.first(fc);
        if (feature == null) {
            throw new ResourceNotFoundException("Could not find a collection named '" + collectionName + "'");
        }

        return feature;
    }

    protected SimpleFeatureType mapFeatureTypeToSimple(FeatureType schema,
            Consumer<SimpleFeatureTypeBuilder> extraAttributeBuilder) {
        SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();
        for (PropertyDescriptor pd : schema.getDescriptors()) {
            // skip multivalue, metadata and quicklook
            final Name propertyName = pd.getName();
            if (pd.getMaxOccurs() > 1 || OpenSearchAccess.METADATA_PROPERTY_NAME.equals(propertyName)
                    || OpenSearchAccess.QUICKLOOK_PROPERTY_NAME.equals(propertyName)
                    || "htmlDescription".equalsIgnoreCase(propertyName.getLocalPart())
                    || "id".equals(propertyName.getLocalPart())) {
                continue;
            }
            Name name = propertyName;
            String uri = name.getNamespaceURI();
            String prefix = null;
            if (OpenSearchAccess.EO_NAMESPACE.equals(uri)) {
                prefix = "eo";
            } else {
                for (ProductClass pc : ProductClass.values()) {
                    if (pc.getNamespace().equals(uri)) {
                        prefix = pc.getPrefix();
                        break;
                    }
                }
            }
            String mappedName;
            if (prefix != null) {
                mappedName = prefix + ":" + name.getLocalPart();
            } else {
                mappedName = name.getLocalPart();
            }
            tb.userData(SOURCE_NAME, name);
            tb.add(mappedName, pd.getType().getBinding());
        }
        tb.setName(schema.getName());
        extraAttributeBuilder.accept(tb);
        SimpleFeatureType targetSchema = tb.buildFeatureType();
        return targetSchema;
    }

    protected OgcLinks buildOgcLinksFromFeature(Feature feature, boolean notFoundOnEmpty) {
        // map to a list of beans
        List<OgcLink> links = Collections.emptyList();
        Collection<Property> linkProperties = feature.getProperties(OpenSearchAccess.OGC_LINKS_PROPERTY_NAME);
        if (linkProperties != null) {
            links = linkProperties.stream().map(p -> (SimpleFeature) p).sorted(LinkFeatureComparator.INSTANCE)
                    .map(sf -> {
                        String offering = (String) sf.getAttribute("offering");
                        String method = (String) sf.getAttribute("method");
                        String code = (String) sf.getAttribute("code");
                        String type = (String) sf.getAttribute("type");
                        String href = (String) sf.getAttribute("href");
                        return new OgcLink(offering, method, code, type, href);
                    }).collect(Collectors.toList());
        }
        if (links.isEmpty() && notFoundOnEmpty) {
            throw new ResourceNotFoundException();
        }
        return new OgcLinks(links);
    }

    protected SimpleFeature mapFeatureToSimple(Feature f, SimpleFeatureType targetSchema,
            Consumer<SimpleFeatureBuilder> extraValueBuilder) {
        SimpleFeatureBuilder fb = new SimpleFeatureBuilder(targetSchema);
        List<AttributeDescriptor> attributeDescriptors = targetSchema.getAttributeDescriptors();
        String identifier = f.getIdentifier().getID();
        for (AttributeDescriptor ad : attributeDescriptors) {
            Name sourceName = (Name) ad.getUserData().get(SOURCE_NAME);
            Property p = f.getProperty(sourceName);
            if (p != null) {
                Object value = p.getValue();
                if (value != null) {
                    fb.set(ad.getLocalName(), value);
                    if (("eo:identifier".equals(ad.getLocalName()) || "eop:identifier".equals(ad.getLocalName()))
                            && value instanceof String) {
                        identifier = (String) value;
                    }
                }
            }
        }
        extraValueBuilder.accept(fb);

        return fb.buildFeature(identifier);
    }

    /**
     * Un-maps OSEO related attributes to a prefix:name for for json encoding
     * 
     * @param fc
     * @return
     */
    protected SimpleFeatureCollection toSimpleFeatureCollection(FeatureCollection<FeatureType, Feature> fc,
            Consumer<SimpleFeatureTypeBuilder> extraAttributeBuilder,
            Consumer<SimpleFeatureBuilder> extraValuesBuilder) {
        SimpleFeatureType targetSchema = mapFeatureTypeToSimple(fc.getSchema(), extraAttributeBuilder);

        return new BaseSimpleFeatureCollection(targetSchema) {

            @Override
            public SimpleFeatureIterator features() {
                FeatureIterator<Feature> features = fc.features();
                return new SimpleFeatureIterator() {

                    @Override
                    public SimpleFeature next() throws NoSuchElementException {
                        Feature f = features.next();
                        return mapFeatureToSimple(f, targetSchema, extraValuesBuilder);
                    }

                    @Override
                    public boolean hasNext() {
                        return features.hasNext();
                    }

                    @Override
                    public void close() {
                        features.close();
                    }
                };
            }
        };
    }

    /**
     * Checks XML well formedness (TODO: check against actual schemas)
     * 
     * @param xml
     * @throws IOException
     * @throws SAXException
     */
    protected void checkWellFormedXML(String xml) {
        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder dBuilder;
        try {
            dBuilder = dbFactory.newDocumentBuilder();
            Document doc = dBuilder.parse(new InputSource(new StringReader(xml)));
        } catch (ParserConfigurationException | SAXException | IOException e) {
            throw new RestException("XML document is not well formed: " + e.getMessage(), HttpStatus.BAD_REQUEST,
                    e);
        }
    }

    /**
     * Factors out the boilerplate to create a transaction, run it, commit it if successful, revert otherwise, and finally close it
     * 
     * @param store
     * @param featureStoreConsumer
     * @throws IOException
     */
    protected void runTransactionOnStore(FeatureStore store, IOConsumer<FeatureStore> featureStoreConsumer)
            throws IOException {
        try (Transaction t = new DefaultTransaction()) {
            store.setTransaction(t);
            try {
                featureStoreConsumer.accept(store);
                t.commit();
            } catch (Exception e) {
                t.rollback();
                throw new IOException("Failed to run modification on storage:" + e.getMessage(), e);
            }
        }
    }

    /**
     * Turns a complex feature into a single item feature collection
     * 
     * @param f
     * @return
     */
    protected FeatureCollection singleton(Feature f) {
        ListComplexFeatureCollection fc = new ListComplexFeatureCollection(f);
        return fc;

    }

    /**
     * Converts the simple feature representatin of a collection into a complex feature suitable for OpenSearchAccess usage
     * 
     * @param feature
     * @return
     * @throws IOException
     */
    protected Feature simpleToComplex(SimpleFeature feature, FeatureType targetSch,
            Collection<String> ignoredAttributes) throws IOException {
        ComplexFeatureBuilder builder = new ComplexFeatureBuilder(targetSch);
        AttributeBuilder ab = new AttributeBuilder(FEATURE_FACTORY);
        for (AttributeDescriptor ad : feature.getType().getAttributeDescriptors()) {
            String sourceName = ad.getLocalName();
            // ignore links
            if (ignoredAttributes.contains(sourceName)) {
                continue;
            }
            // map to complex feature attribute and check
            Name pname = toName(sourceName, targetSch.getName().getNamespaceURI());
            PropertyDescriptor pd = targetSch.getDescriptor(pname);
            if (pd == null) {
                throw new RestException("Unexpected attribute found: '" + sourceName + "'", HttpStatus.BAD_REQUEST);
            }

            ab.setDescriptor((AttributeDescriptor) pd);
            Attribute attribute = ab.buildSimple(null, feature.getAttribute(sourceName));
            builder.append(pd.getName(), attribute);
        }
        Feature collectionFeature = builder.buildFeature(feature.getID());
        return collectionFeature;
    }

    protected Name toName(String sourceName, String defaultNamespace) {
        String[] split = sourceName.split(":");
        switch (split.length) {
        case 1:
            if ("geometry".equals(sourceName)) {
                return new NameImpl(defaultNamespace, "footprint");
            } else {
                return new NameImpl(defaultNamespace, sourceName);
            }
        case 2:
            String prefix = split[0];
            String localName = split[1];
            String namespaceURI = null;
            if ("eo".equals(prefix)) {
                namespaceURI = OpenSearchAccess.EO_NAMESPACE;
            } else {
                for (OpenSearchAccess.ProductClass pc : OpenSearchAccess.ProductClass.values()) {
                    if (prefix.equals(pc.getPrefix())) {
                        namespaceURI = pc.getNamespace();
                    }
                }
            }

            if (namespaceURI == null) {
                throw new RestException("Unrecognized attribute prefix in property " + sourceName,
                        HttpStatus.BAD_REQUEST);
            }

            return new NameImpl(namespaceURI, localName);
        default:
            throw new RestException("Unrecognized attribute " + sourceName, HttpStatus.BAD_REQUEST);
        }
    }

    /**
     * Builds the feature type exposed to client for OGC links
     * 
     * @return
     * @throws IOException
     */
    protected SimpleFeatureType buildOgcLinksType() throws IOException {
        String ns = getOpenSearchAccess().getCollectionSource().getName().getNamespaceURI();
        SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();
        tb.setName("ogc_links");
        tb.setNamespaceURI(ns);
        tb.add("offering", String.class);
        tb.add("method", String.class);
        tb.add("code", String.class);
        tb.add("type", String.class);
        tb.add("href", String.class);
        return tb.buildFeatureType();
    }

    protected ListFeatureCollection beansToLinksCollection(OgcLinks links) throws IOException {
        SimpleFeatureType schema = buildOgcLinksType();
        ListFeatureCollection linksCollection = new ListFeatureCollection(schema);
        SimpleFeatureBuilder fb = new SimpleFeatureBuilder(schema);
        for (OgcLink link : links.links) {
            fb.set("offering", link.offering);
            fb.set("method", link.method);
            fb.set("code", link.code);
            fb.set("type", link.type);
            fb.set("href", link.href);
            SimpleFeature sf = fb.buildFeature(null);
            linksCollection.add(sf);
        }
        return linksCollection;
    }

    protected <T extends ZipPart> Map<T, byte[]> parsePartsFromZip(InputStream body, T[] parts) throws IOException {
        // check the zip contents and map to the expected parts
        Map<T, byte[]> result = new HashMap<>();
        try {
            ZipInputStream zis = new ZipInputStream(body);
            ZipEntry entry = null;

            while ((entry = zis.getNextEntry()) != null) {
                String name = entry.getName();
                T part = null;
                for (T zp : parts) {
                    if (zp.matches(name)) {
                        part = zp;
                        break;
                    }
                }
                if (part != null) {
                    result.put(part, IOUtils.toByteArray(zis));
                } else {
                    LOGGER.warning("Ignoring un-recognized entry in zip file:" + name);
                }
            }
        } catch (ZipException e) {
            throw new RestException(e.getMessage(), HttpStatus.BAD_REQUEST);
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    protected <T> T parseJSON(Class<T> clazz, byte[] rawData) throws IOException {
        T links = (T) jsonConverter.read(clazz, new HttpInputMessage() {

            @Override
            public HttpHeaders getHeaders() {
                return new HttpHeaders();
            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream(rawData);
            }
        });
        return links;
    }

    protected SimpleFeature parseGeoJSONFeature(String fileReference, final byte[] payload) {
        try {
            SimpleFeature jsonFeature = new FeatureJSON().readFeature(new ByteArrayInputStream(payload));
            return jsonFeature;
        } catch (IOException e) {
            throw new RestException(fileReference + " contains invalid GeoJSON: " + e.getMessage(),
                    HttpStatus.BAD_REQUEST, e);
        }
    }

    protected SimpleFeatureCollection parseGeoJSONFeatureCollection(String fileReference, final byte[] payload) {
        try {
            SimpleFeatureCollection fc = (SimpleFeatureCollection) new FeatureJSON()
                    .readFeatureCollection(new ByteArrayInputStream(payload));
            return fc;
        } catch (IOException e) {
            throw new RestException(fileReference + " contains invalid GeoJSON: " + e.getMessage(),
                    HttpStatus.BAD_REQUEST, e);
        }
    }

}