/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.csw.records;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.net.URL;
import java.util.List;
import net.opengis.cat.csw20.ElementSetType;
import org.geoserver.csw.util.NamespaceQualifier;
import org.geoserver.platform.GeoServerExtensions;
import org.geotools.csw.CSW;
import org.geotools.csw.DC;
import org.geotools.csw.DCT;
import org.geotools.data.Query;
import org.geotools.data.complex.config.EmfComplexFeatureReader;
import org.geotools.data.complex.config.FeatureTypeRegistry;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.feature.NameImpl;
import org.geotools.feature.TypeBuilder;
import org.geotools.feature.type.FeatureTypeFactoryImpl;
import org.geotools.feature.type.Types;
import org.geotools.filter.SortByImpl;
import org.geotools.filter.spatial.DefaultCRSFilterVisitor;
import org.geotools.filter.spatial.ReprojectingFilterVisitor;
import org.geotools.filter.v1_0.OGC;
import org.geotools.ows.OWS;
import org.geotools.xml.SchemaIndex;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.AttributeType;
import org.opengis.feature.type.ComplexType;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.FeatureTypeFactory;
import org.opengis.feature.type.Name;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.sort.SortBy;
import org.xml.sax.helpers.NamespaceSupport;
import com.vividsolutions.jts.geom.MultiPolygon;
/**
* Describes the CSW records and provides some handy constants to help building features
* representing CSW:Record.
*
* A few remarks about the {@link #RECORD_TYPE} feature type:
* <ul>
* <li>The SimpleLiterals are complex elements with simple contents, which we cannot properly
* represent in GeoTools as the moment, the adopted solution is to have SimpleLiteral sport two
* properties, value and scheme, which means the property paths in filters and sort operations have
* to be adapted from <code>dc(t):elementName</code> to <code>dc(t):elementName/dc:value</code>
* <li>The ows:BoundingBox element can be repeated multiple times and can have different SRS in each
* instance, to deal with that we build a single geometry, a multipolygon, and keep the original
* bounding boxes in the attribute user data, under the {@link #RECORD_BBOX_DESCRIPTOR} key</li>
* </ul>
*
* @author Andrea Aime - GeoSolutions
*/
public class CSWRecordDescriptor extends AbstractRecordDescriptor {
private static FilterFactory2 FF = CommonFactoryFinder.getFilterFactory2();
/**
* Contains the declarations of common namespaces and prefixes used in the CSW world
*/
public static final Name SIMPLE_LITERAL_SCHEME = new NameImpl(DC.NAMESPACE, "scheme");
public static final Name SIMPLE_LITERAL_VALUE = new NameImpl(DC.NAMESPACE, "value");
public static final Name DC_ELEMENT_NAME = new NameImpl(DC.NAMESPACE, DC.DCelement.getLocalPart());
public static final NameImpl RECORD_BBOX_NAME = new NameImpl(OWS.NAMESPACE, "BoundingBox");
public static final List<Name> BRIEF_ELEMENTS;
public static final List<Name> SUMMARY_ELEMENTS;
public static final NamespaceSupport NAMESPACES;
public static final ComplexType SIMPLE_LITERAL;
public static final AttributeDescriptor DC_ELEMENT;
public static final AttributeDescriptor RECORD_BBOX_DESCRIPTOR;
public static final FeatureType RECORD_TYPE;
public static final AttributeDescriptor RECORD_DESCRIPTOR;
static final CRSRecordProjectyPathAdapter PATH_EXTENDER;
static final NamespaceQualifier NSS_QUALIFIER;
static final DefaultCRSFilterVisitor CRS_DEFAULTER;
static final ReprojectingFilterVisitor CRS_REPROJECTOR;
public static final List<Name> QUERIABLES;
static {
// prepare the common namespace support
NAMESPACES = new NamespaceSupport();
NAMESPACES.declarePrefix("csw", CSW.NAMESPACE);
NAMESPACES.declarePrefix("rim", "urn:oasis:names:tc:ebxml-regrep:xsd:rim:3.0");
NAMESPACES.declarePrefix("dc", DC.NAMESPACE);
NAMESPACES.declarePrefix("dct", DCT.NAMESPACE);
NAMESPACES.declarePrefix("ows", OWS.NAMESPACE);
NAMESPACES.declarePrefix("ogc", OGC.NAMESPACE);
// prepare the CSW record related types
FeatureTypeFactory typeFactory = new FeatureTypeFactoryImpl();
TypeBuilder builder = new TypeBuilder(typeFactory);
// create the SimpleLiteral type
builder.setNamespaceURI(DC.SimpleLiteral.getNamespaceURI());
builder.name("scheme");
builder.bind(URI.class);
AttributeType schemeType = builder.attribute();
builder.setNamespaceURI(DC.SimpleLiteral.getNamespaceURI());
builder.name("value");
builder.bind(Object.class);
AttributeType valueType = builder.attribute();
builder.setNillable(true);
builder.addAttribute("scheme", schemeType);
builder.addAttribute("value", valueType);
builder.setName("SimpleLiteral");
SIMPLE_LITERAL = builder.complex();
builder.setNamespaceURI(OWS.NAMESPACE);
builder.setName("BoundingBoxType");
builder.setBinding(MultiPolygon.class);
builder.crs(DEFAULT_CRS);
AttributeType bboxType = builder.geometry();
EmfComplexFeatureReader reader = EmfComplexFeatureReader.newInstance();
SchemaIndex index = null;
try {
index = reader.parse(new URL("http://schemas.opengis.net/csw/2.0.2/record.xsd"));
} catch (IOException e) {
//this is fatal
throw new RuntimeException("Failed to parse CSW Record Schemas" , e);
}
FeatureTypeRegistry featureTypeRegistry = new FeatureTypeRegistry(NAMESPACES, typeFactory, new RecordFeatureTypeRegistryConfiguration("RecordType"));
featureTypeRegistry.register(SIMPLE_LITERAL);
featureTypeRegistry.register(bboxType);
featureTypeRegistry.addSchemas(index);
RECORD_TYPE = (FeatureType) featureTypeRegistry.getAttributeType(new NameImpl(
CSW.NAMESPACE, "RecordType"));
RECORD_DESCRIPTOR = featureTypeRegistry.getDescriptor(new NameImpl(CSW.NAMESPACE, "Record"), null);
RECORD_BBOX_DESCRIPTOR = (AttributeDescriptor) RECORD_TYPE.getDescriptor(RECORD_BBOX_NAME);
DC_ELEMENT = (AttributeDescriptor) RECORD_TYPE.getDescriptor(DC_ELEMENT_NAME);
//---
// setup the list of names for brief and summary records
BRIEF_ELEMENTS = createNameList(NAMESPACES, "dc:identifier", "dc:title", "dc:type", "ows:BoundingBox");
SUMMARY_ELEMENTS = createNameList(NAMESPACES, "dc:identifier", "dc:title", "dc:type", "dc:subject",
"dc:format", "dc:relation", "dct:modified", "dct:abstract", "dct:spatial", "ows:BoundingBox");
// create the xpath extender that fill adapt dc:title to dc:title/dc:value
PATH_EXTENDER = new CRSRecordProjectyPathAdapter(NAMESPACES);
// qualified the xpath in the filters
NSS_QUALIFIER = new NamespaceQualifier(NAMESPACES);
// applies the default CRS to geometry filters coming from the outside
CRS_DEFAULTER = new DefaultCRSFilterVisitor(FF, DEFAULT_CRS);
// transforms geometry filters into the internal representation
CRS_REPROJECTOR = new ReprojectingFilterVisitor(FF, RECORD_TYPE);
//build queriables list
QUERIABLES = createNameList(NAMESPACES, "dc:contributor", "dc:source", "dc:language",
"dc:title", "dc:subject", "dc:creator", "dc:type", "ows:BoundingBox", "dct:modified",
"dct:abstract", "dc:relation", "dc:date", "dc:identifier", "dc:publisher",
"dc:format", "csw:AnyText", "dc:rights");
}
/**
* Checks if a field is public static final
*
* @param field
*
*/
static boolean isConstant(Field field) {
int modifier = field.getModifiers();
return Modifier.isStatic(modifier) && Modifier.isPublic(modifier)
&& Modifier.isFinal(modifier);
}
@Override
public AttributeDescriptor getFeatureDescriptor() {
return RECORD_DESCRIPTOR;
}
@Override
public String getOutputSchema() {
return CSW.NAMESPACE;
}
@Override
public List<Name> getPropertiesForElementSet(ElementSetType elementSet) {
switch (elementSet) {
case BRIEF:
return CSWRecordDescriptor.BRIEF_ELEMENTS;
case SUMMARY:
return CSWRecordDescriptor.SUMMARY_ELEMENTS;
default:
return null;
}
}
@Override
public NamespaceSupport getNamespaceSupport() {
return NAMESPACES;
}
@Override
public Query adaptQuery(Query query) {
Filter filter = query.getFilter();
if(filter != null && !Filter.INCLUDE.equals(filter)) {
Filter qualified = (Filter) filter.accept(NSS_QUALIFIER, null);
Filter extended = (Filter) qualified.accept(PATH_EXTENDER, null);
query.setFilter(extended);
}
SortBy[] sortBy = query.getSortBy();
if(sortBy != null && sortBy.length > 0) {
CSWPropertyPathExtender extender = new CSWPropertyPathExtender();
for (int i = 0; i < sortBy.length; i++) {
SortBy sb = sortBy[i];
if(!SortBy.NATURAL_ORDER.equals(sb) && !SortBy.REVERSE_ORDER.equals(sb)) {
PropertyName name = sb.getPropertyName();
PropertyName extended = extender.extendProperty(name, FF, NAMESPACES);
sortBy[i] = new SortByImpl(extended, sb.getSortOrder());
}
}
query.setSortBy(sortBy);
}
return query;
}
@Override
public String getBoundingBoxPropertyName() {
return "BoundingBox";
}
/**
* Locates the AttributeDescriptor corresponding to the specified element name
*/
public static AttributeDescriptor getDescriptor(String elementName) {
return (AttributeDescriptor) Types.findDescriptor(RECORD_TYPE, elementName);
}
@Override
public List<Name> getQueryables() {
return QUERIABLES;
}
@Override
public String getQueryablesDescription() {
return "SupportedDublinCoreQueryables";
}
@Override
public PropertyName translateProperty(Name name) {
return new CSWPropertyPathExtender().extendProperty(buildPropertyName(NAMESPACES, name), FF, NAMESPACES);
}
public void verifySpatialFilters(Filter filter) {
filter.accept(new SpatialFilterChecker(getFeatureType()), null);
}
//singleton
private CSWRecordDescriptor() {}
private static CSWRecordDescriptor INSTANCE;
public static CSWRecordDescriptor getInstance() {
if (INSTANCE == null) {
//if there is a bean available, use the bean otherwise create other
INSTANCE = GeoServerExtensions.bean(CSWRecordDescriptor.class);
if (INSTANCE == null) {
INSTANCE = new CSWRecordDescriptor();
}
}
return INSTANCE;
}
}