/* (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.eo.store; import static org.geoserver.opensearch.eo.store.OpenSearchAccess.METADATA_PROPERTY_NAME; import static org.geoserver.opensearch.eo.store.OpenSearchAccess.OGC_LINKS_PROPERTY_NAME; import java.awt.RenderingHints.Key; import java.io.IOException; import java.net.URI; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; import java.util.logging.Logger; import org.geotools.data.DataAccess; import org.geotools.data.DataSourceException; import org.geotools.data.DefaultResourceInfo; import org.geotools.data.FeatureListener; import org.geotools.data.FeatureSource; import org.geotools.data.Join; import org.geotools.data.Join.Type; import org.geotools.data.Query; import org.geotools.data.QueryCapabilities; import org.geotools.data.ResourceInfo; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureSource; import org.geotools.data.store.EmptyFeatureCollection; import org.geotools.factory.CommonFactoryFinder; import org.geotools.feature.AttributeBuilder; import org.geotools.feature.ComplexFeatureBuilder; import org.geotools.feature.FeatureCollection; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.util.logging.Logging; import org.opengis.feature.Attribute; import org.opengis.feature.Feature; import org.opengis.feature.FeatureVisitor; 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.Filter; import org.opengis.filter.FilterFactory2; import org.opengis.filter.expression.PropertyName; import org.opengis.filter.identity.FeatureId; import org.opengis.filter.sort.SortBy; import org.opengis.filter.sort.SortOrder; /** * Base class for the collection and product specific feature source wrappers * * @author Andrea Aime - GeoSolutions */ public abstract class AbstractMappingSource implements FeatureSource<FeatureType, Feature> { static final Logger LOGGER = Logging.getLogger(AbstractMappingSource.class); protected static FilterFactory2 FF = CommonFactoryFinder.getFilterFactory2(); protected JDBCOpenSearchAccess openSearchAccess; protected FeatureType schema; protected SourcePropertyMapper propertyMapper; protected SortBy[] defaultSort; private SimpleFeatureType linkFeatureType; public AbstractMappingSource(JDBCOpenSearchAccess openSearchAccess, FeatureType collectionFeatureType) throws IOException { this.openSearchAccess = openSearchAccess; this.schema = collectionFeatureType; this.propertyMapper = new SourcePropertyMapper(schema); this.defaultSort = buildDefaultSort(schema); this.linkFeatureType = buildLinkFeatureType(); } protected SimpleFeatureType buildLinkFeatureType() throws IOException { SimpleFeatureType source = openSearchAccess.getDelegateStore().getSchema(getLinkTable()); try { SimpleFeatureTypeBuilder b = new SimpleFeatureTypeBuilder(); b.init(source); b.setName(openSearchAccess.OGC_LINKS_PROPERTY_NAME); return b.buildFeatureType(); } catch (Exception e) { throw new DataSourceException("Could not build the renamed feature type.", e); } } /** * Builds the default sort for the underlying feature source query * * @param schema * @return */ protected SortBy[] buildDefaultSort(FeatureType schema) { String timeStart = propertyMapper.getSourceName("timeStart"); String identifier = propertyMapper.getSourceName("identifier"); return new SortBy[] { FF.sort(timeStart, SortOrder.DESCENDING), FF.sort(identifier, SortOrder.ASCENDING) }; } @Override public Name getName() { return schema.getName(); } @Override public ResourceInfo getInfo() { try { SimpleFeatureSource featureSource = getDelegateCollectionSource(); ResourceInfo delegateInfo = featureSource.getInfo(); DefaultResourceInfo result = new DefaultResourceInfo(delegateInfo); result.setSchema(new URI(schema.getName().getNamespaceURI())); return result; } catch (RuntimeException ex) { throw ex; } catch (Exception ex) { throw new RuntimeException(ex); } } /* * + Returns the underlying delegate source */ protected abstract SimpleFeatureSource getDelegateCollectionSource() throws IOException; @Override public DataAccess<FeatureType, Feature> getDataStore() { return openSearchAccess; } @Override public QueryCapabilities getQueryCapabilities() { QueryCapabilities result = new QueryCapabilities() { @Override public boolean isOffsetSupported() { return true; } @Override public boolean isReliableFIDSupported() { // the delegate store should have a primary key on collections return true; } }; return result; } @Override public void addFeatureListener(FeatureListener listener) { throw new UnsupportedOperationException(); } @Override public void removeFeatureListener(FeatureListener listener) { throw new UnsupportedOperationException(); } @Override public FeatureCollection<FeatureType, Feature> getFeatures(Filter filter) throws IOException { return getFeatures(new Query(getSchema().getName().getLocalPart(), filter)); } @Override public FeatureCollection<FeatureType, Feature> getFeatures() throws IOException { return getFeatures(Query.ALL); } @Override public FeatureType getSchema() { return schema; } @Override public ReferencedEnvelope getBounds() throws IOException { return getDelegateCollectionSource().getBounds(); } @Override public ReferencedEnvelope getBounds(Query query) throws IOException { Query mapped = mapToSimpleCollectionQuery(query, false); return getDelegateCollectionSource().getBounds(mapped); } @Override public Set<Key> getSupportedHints() { try { return getDelegateCollectionSource().getSupportedHints(); } catch (RuntimeException ex) { throw ex; } catch (Exception ex) { throw new RuntimeException(ex); } } @Override public int getCount(Query query) throws IOException { final Query mappedQuery = mapToSimpleCollectionQuery(query, false); return getDelegateCollectionSource().getCount(mappedQuery); } /** * Maps query back the main underlying feature source * * @param query * @return * @throws IOException */ protected Query mapToSimpleCollectionQuery(Query query, boolean addJoins) throws IOException { Query result = new Query(getDelegateCollectionSource().getSchema().getTypeName()); if (query.getFilter() != null) { MappingFilterVisitor visitor = new MappingFilterVisitor(propertyMapper); Filter mappedFilter = (Filter) query.getFilter().accept(visitor, null); result.setFilter(mappedFilter); } if (query.getPropertyNames() != null && query.getPropertyNames().length > 0) { String[] mappedPropertyNames = Arrays.stream(query.getPropertyNames()) .map(name -> propertyMapper.getSourceName(name)).filter(name -> name != null) .toArray(size -> new String[size]); if (mappedPropertyNames.length == 0) { result.setPropertyNames(Query.ALL_NAMES); } else { result.setPropertyNames(mappedPropertyNames); } } if (query.getSortBy() != null && query.getSortBy().length > 0) { SortBy[] mappedSortBy = Arrays.stream(query.getSortBy()).map(sb -> { if (sb == SortBy.NATURAL_ORDER || sb == SortBy.REVERSE_ORDER) { return sb; } else { String name = sb.getPropertyName().getPropertyName(); String mappedName = propertyMapper.getSourceName(name); if (mappedName == null) { throw new IllegalArgumentException("Cannot sort on " + name); } return FF.sort(mappedName, sb.getSortOrder()); } }).toArray(size -> new SortBy[size]); result.setSortBy(mappedSortBy); } else { // get stable results for paging result.setSortBy(defaultSort); } if(addJoins) { // join to metadata table if necessary if (hasOutputProperty(query, METADATA_PROPERTY_NAME, false)) { Filter filter = FF.equal(FF.property("id"), FF.property("metadata.mid"), true); final String metadataTable = getMetadataTable(); Join join = new Join(metadataTable, filter); join.setAlias("metadata"); result.getJoins().add(join); } // same goes for OGC links (they might be missing, so outer join is used) if (hasOutputProperty(query, OGC_LINKS_PROPERTY_NAME, true)) { final String linkTable = getLinkTable(); final String linkForeignKey = getLinkForeignKey(); Filter filter = FF.equal(FF.property("id"), FF.property("link." + linkForeignKey), true); Join join = new Join(linkTable, filter); join.setAlias("link"); join.setType(Type.OUTER); result.getJoins().add(join); } } else { // only non joined requests are pageable result.setStartIndex(query.getStartIndex()); result.setMaxFeatures(query.getMaxFeatures()); } return result; } /** * Name of the metadata table to join in case the {@link OpenSearchAccess#METADATA_PROPERTY_NAME} property is requested * * @return */ protected abstract String getMetadataTable(); /** * Name of the link table to join in case the {@link OpenSearchAccess#OGC_LINKS_PROPERTY_NAME} property is requested * * @return */ protected abstract String getLinkTable(); /** * Name of the field linking back to the main table in case the {@link OpenSearchAccess#OGC_LINKS_PROPERTY_NAME} property is requested * * @return */ protected abstract String getLinkForeignKey(); /** * Searches for an optional property among the query attributes. Returns true only if the property is explicitly listed * * @param query * @param property * @return */ protected boolean hasOutputProperty(Query query, Name property, boolean includedByDefault) { if (query.getProperties() == null) { return includedByDefault; } for (PropertyName pn : query.getProperties()) { if (property.getLocalPart().equals(pn.getPropertyName()) && property.getNamespaceURI().equals(pn.getNamespaceContext().getURI(""))) { return true; } } return false; } @Override public FeatureCollection<FeatureType, Feature> getFeatures(Query query) throws IOException { // first get the ids of the features we are going to return, no joins to support paging Query idsQuery = mapToSimpleCollectionQuery(query, false); // idsQuery.setProperties(Query.NO_PROPERTIES); (no can do, there are mandatory fields) SimpleFeatureCollection idFeatureCollection = getDelegateCollectionSource().getFeatures(idsQuery); Set<FeatureId> ids = new LinkedHashSet<>(); idFeatureCollection.accepts(f -> ids.add(f.getIdentifier()), null); // if no features, return immediately SimpleFeatureCollection fc; if(ids.isEmpty()) { fc = new EmptyFeatureCollection(getDelegateCollectionSource().getSchema()); } else { // the run a joined query with the specified ids Query dataQuery = mapToSimpleCollectionQuery(query, true); dataQuery.setFilter(FF.id(ids)); fc = getDelegateCollectionSource().getFeatures(dataQuery); } return new MappingFeatureCollection(schema, fc, this::mapToComplexFeature); } /** * Maps the underlying features (eventually joined) to the output complex feature * * @param it * @return */ protected Feature mapToComplexFeature(PushbackFeatureIterator<SimpleFeature> it) { SimpleFeature fi = it.next(); ComplexFeatureBuilder builder = new ComplexFeatureBuilder(schema); // allow subclasses to perform custom mappings while reusing the common ones mapProperties(builder, fi); // the OGC links can be more than one Object link = fi.getAttribute("link"); while(link instanceof SimpleFeature) { // retype the feature to have the right name SimpleFeature linkFeature = SimpleFeatureBuilder.retype((SimpleFeature) link, linkFeatureType); builder.append(OGC_LINKS_PROPERTY_NAME, linkFeature); // see if there are more links if(it.hasNext()) { SimpleFeature next = it.next(); // same feature? if(next.getID().equals(fi.getID())) { link = next.getAttribute("link"); } else { // moved to the next feature, push it back, // we're done for the current one it.pushBack(); break; } } else { break; } } // Feature feature = builder.buildFeature(fi.getID()); return feature; } /** * Performs the common mappings, subclasses can override to add more * * @param builder * @param fi */ protected void mapProperties(ComplexFeatureBuilder builder, SimpleFeature fi) { AttributeBuilder ab = new AttributeBuilder(CommonFactoryFinder.getFeatureFactory(null)); for (PropertyDescriptor pd : schema.getDescriptors()) { if (!(pd instanceof AttributeDescriptor)) { continue; } String localName = (String) pd.getUserData().get(JDBCOpenSearchAccess.SOURCE_ATTRIBUTE); if (localName == null) { continue; } Object value = fi.getAttribute(localName); if (value == null) { continue; } ab.setDescriptor((AttributeDescriptor) pd); Attribute attribute = ab.buildSimple(null, value); builder.append(pd.getName(), attribute); } // handle joined metadata Object metadataValue = fi.getAttribute("metadata"); if (metadataValue instanceof SimpleFeature) { SimpleFeature metadataFeature = (SimpleFeature) metadataValue; ab.setDescriptor((AttributeDescriptor) schema .getDescriptor(METADATA_PROPERTY_NAME)); Attribute attribute = ab.buildSimple(null, metadataFeature.getAttribute("metadata")); builder.append(METADATA_PROPERTY_NAME, attribute); } } }