/* (c) 2015 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.importer.format;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.apache.commons.io.FilenameUtils;
import org.geoserver.catalog.AttributeTypeInfo;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CatalogBuilder;
import org.geoserver.catalog.CatalogFactory;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.catalog.StoreInfo;
import org.geoserver.catalog.WorkspaceInfo;
import org.geoserver.importer.ImportData;
import org.geoserver.importer.ImportTask;
import org.geoserver.importer.VectorFormat;
import org.geoserver.importer.job.ProgressMonitor;
import org.geoserver.importer.transform.ReprojectTransform;
import org.geoserver.importer.transform.TransformChain;
import org.geoserver.importer.transform.VectorTransform;
import org.geoserver.importer.transform.VectorTransformChain;
import org.geotools.data.FeatureReader;
import org.geotools.factory.Hints;
import org.geotools.feature.AttributeTypeBuilder;
import org.geotools.feature.FeatureTypes;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.gml3.v3_2.GML;
import org.geotools.referencing.CRS;
import org.geotools.referencing.CRS.AxisOrder;
import org.geotools.util.ConverterFactory;
import org.geotools.util.Converters;
import org.geotools.wfs.v1_0.WFSConfiguration;
import org.geotools.xml.Configuration;
import org.geotools.xml.PullParser;
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.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import com.vividsolutions.jts.geom.Geometry;
/**
* Supports reading GML simple features from a file with ".gml" extension
*
* @author Andrea Aime - GeoSolutions
*
*/
public class GMLFileFormat extends VectorFormat {
private static final Class[] TYPE_GUESS_TARGETS = new Class[] { Integer.class, Long.class,
Double.class, Boolean.class, Date.class };
private static final HashSet<Class> VALID_ATTRIBUTE_TYPES = new HashSet<>(Arrays.asList(
(Class) Geometry.class, Number.class, Date.class, Boolean.class, String.class));
private static final List<String> GML_ATTRIBUTES = Arrays.asList("name", "description",
"boundedBy", "location");
private static final Map<Class, Class> TYPE_PROMOTIONS = new HashMap<Class, Class>() {
{
put(Integer.class, Long.class);
put(Long.class, Double.class);
}
};
private static final String GML_VERSION_KEY = "version";
private static final String GENERIC_2D_CODE = "EPSG:404000";
enum GMLVersion {
// use the wfs configurations, as they contain the gml ones
GML2(new WFSConfiguration()), GML3(new org.geotools.wfs.v1_1.WFSConfiguration()), GML32(
new org.geotools.wfs.v2_0.WFSConfiguration());
Configuration configuration;
private GMLVersion(Configuration configuration) {
this.configuration = configuration;
}
public Configuration getConfiguration() {
return configuration;
}
};
@Override
public FeatureReader read(ImportData data, ImportTask task) throws IOException {
File file = getFileFromData(data);
// we need to get the feature type, to use for the particular parse through the file
// since we put it on the metadata from the list method, we first check if that's still
// available
SimpleFeatureType ft = (SimpleFeatureType) task.getMetadata().get(FeatureType.class);
GMLVersion version = (GMLVersion) task.getMetadata().get(GML_VERSION_KEY);
if (version == null) {
version = GMLVersion.GML3;
}
if (ft == null) {
FeatureTypeInfo fti = (FeatureTypeInfo) task.getLayer().getResource();
ft = buildFeatureTypeFromInfo(fti);
}
return new GMLReader(new FileInputStream(file), version.getConfiguration(), ft);
}
@Override
public void dispose(FeatureReader reader, ImportTask item) throws IOException {
reader.close();
}
@Override
public List<ImportTask> list(ImportData data, Catalog catalog, ProgressMonitor monitor)
throws IOException {
File file = getFileFromData(data);
SimpleFeatureType featureType = getSchema(file);
CatalogFactory factory = catalog.getFactory();
CatalogBuilder cb = new CatalogBuilder(catalog);
String name = featureType.getName().getLocalPart();
FeatureTypeInfo ftinfo = factory.createFeatureType();
ftinfo.setEnabled(true);
ftinfo.setNativeName(name);
ftinfo.setName(name);
ftinfo.setTitle(name);
ftinfo.setNamespace(catalog.getDefaultNamespace());
List<AttributeTypeInfo> attributes = ftinfo.getAttributes();
for (AttributeDescriptor ad : featureType.getAttributeDescriptors()) {
AttributeTypeInfo att = factory.createAttribute();
att.setName(ad.getLocalName());
att.setBinding(ad.getType().getBinding());
attributes.add(att);
}
LayerInfo layer = cb.buildLayer((ResourceInfo) ftinfo);
ResourceInfo resource = layer.getResource();
CoordinateReferenceSystem crs = featureType.getCoordinateReferenceSystem();
CoordinateReferenceSystem targetCRS = crs;
if (crs == null) {
resource.setSRS(GENERIC_2D_CODE);
resource.setNativeCRS(null);
} else {
Integer code = null;
try {
code = CRS.lookupEpsgCode(crs, true);
} catch (FactoryException e) {
throw (IOException) new IOException().initCause(e);
}
try {
// if we could not find a code, reproject to a target CRS
if (code == null) {
targetCRS = CRS.decode("EPSG:4326", true);
resource.setSRS("EPSG:4326");
resource.setNativeCRS(crs);
} else if (CRS.getAxisOrder(crs) == AxisOrder.NORTH_EAST) {
targetCRS = CRS.decode("EPSG:" + code, true);
resource.setSRS("EPSG:" + code);
resource.setNativeCRS(targetCRS);
} else {
resource.setSRS("EPSG:" + code);
resource.setNativeCRS(crs);
}
} catch (Exception e) {
throw new IOException("Failed to setup the layer CRS", e);
}
}
resource.setNativeBoundingBox(EMPTY_BOUNDS);
resource.setLatLonBoundingBox(EMPTY_BOUNDS);
resource.getMetadata().put("recalculate-bounds", Boolean.TRUE);
ImportTask task = new ImportTask(data);
task.setLayer(layer);
task.getMetadata().put(FeatureType.class, featureType);
task.getMetadata().put(GML_VERSION_KEY, featureType.getUserData().get(GML_VERSION_KEY));
// in case the native CRS was not usable
if (targetCRS != crs) {
ReprojectTransform transform = new ReprojectTransform(crs, targetCRS);
TransformChain<VectorTransform> chain = new VectorTransformChain(transform);
task.setTransform(chain);
}
return Collections.singletonList(task);
}
SimpleFeatureType getSchema(File file) throws IOException {
// do we have a schema location?
boolean hasSchema = false;
GMLVersion version = GMLVersion.GML3;
try (FileReader input = new FileReader(file)) {
// create a pull parser
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
factory.setValidating(false);
// parse root element
XmlPullParser parser = factory.newPullParser();
// parser.setInput(input, "UTF-8");
parser.setInput(input);
parser.nextTag();
String location = parser.getAttributeValue("http://www.w3.org/2001/XMLSchema-instance",
"schemaLocation");
hasSchema = location != null;
String gmlNamespace = parser.getNamespace("gml");
if (GML.NAMESPACE.equals(gmlNamespace)) {
version = GMLVersion.GML32;
} else {
// missing, or the generic "http://opengis.net/gml" used for GML 3.1 and 2.x grrr
// try to use some version detection based on heuristics (e.g., tags that
// we know are specific to a particular version). These could certainly use some
// improvement...
while (parser.next() != XmlPullParser.END_DOCUMENT) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String tag = parser.getName();
String ns = parser.getNamespace();
if ("outerBoundaryIs".equals(tag) || "innerBoundaryIs".equals(tag)) {
version = GMLVersion.GML2;
break;
}
}
}
} catch (XmlPullParserException e) {
throw new IOException("Failed to parse the input file", e);
}
// parse the xml and figure out the feature type
String typeName = null;
Map<String, AttributeDescriptor> guessedTypes = new HashMap<>();
SimpleFeatureType result = null;
try (FileInputStream fis = new FileInputStream(file)) {
SimpleFeature sf = null;
PullParser parser = new PullParser(version.getConfiguration(), fis, SimpleFeature.class);
sf = (SimpleFeature) parser.parse();
while (sf != null) {
if (hasSchema) {
// we trust the feature type found by the parser then, but we still
// have to figure out the CRS
result = sf.getFeatureType();
if (result.getCoordinateReferenceSystem() == null) {
Geometry g = (Geometry) sf.getDefaultGeometry();
if (g != null && g.getUserData() instanceof CoordinateReferenceSystem) {
CoordinateReferenceSystem crs = (CoordinateReferenceSystem) g
.getUserData();
result = FeatureTypes.transform(result, crs);
}
}
}
// even if we have the schema, we figure out the attributes anyways
// since we need to decide what to do of the GML base ones
if (typeName == null) {
typeName = sf.getFeatureType().getTypeName();
}
for (AttributeDescriptor ad : sf.getFeatureType().getAttributeDescriptors()) {
String name = ad.getLocalName();
updateSimpleTypeGuess(name, sf.getAttribute(name), guessedTypes);
}
// move to next feature
sf = (SimpleFeature) parser.parse();
}
} catch (Exception e) {
throw new IOException("Failed to parse GML data", e);
}
// did we use the features own feature types?
if (result != null) {
SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();
tb.init(result);
for (String gmlAttribute : GML_ATTRIBUTES) {
if (!guessedTypes.containsKey(gmlAttribute) && tb.get(gmlAttribute) != null) {
tb.remove(gmlAttribute);
}
}
for (AttributeDescriptor ad : result.getAttributeDescriptors()) {
String name = ad.getLocalName();
Class<?> binding = ad.getType().getBinding();
boolean valid = false;
for (Class validAttributeType : VALID_ATTRIBUTE_TYPES) {
if (validAttributeType.isAssignableFrom(binding)) {
valid = true;
break;
}
}
if (!valid && tb.get(name) != null) {
tb.remove(name);
}
}
result = tb.buildFeatureType();
}
// ok, the gml was schema-less and we figured out the type structure with heuristics then
if (typeName != null) {
SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();
tb.setName(typeName);
for (AttributeDescriptor ad : guessedTypes.values()) {
tb.add(ad);
}
result = tb.buildFeatureType();
} else {
// uh oh, empty GML file?
throw new IllegalArgumentException("Could not find any GML feature in the file");
}
result.getUserData().put(GML_VERSION_KEY, version);
return result;
}
private void updateSimpleTypeGuess(String name, Object value,
Map<String, AttributeDescriptor> guessedTypes) {
if (value == null) {
return;
}
// if we have already established it's a string, bail out
AttributeDescriptor ad = guessedTypes.get(name);
Class target = null;
if (ad != null) {
target = ad.getType().getBinding();
}
Class originalTarget = target;
if (String.class.equals(target) || Geometry.class.equals(target)) {
return;
}
if (value instanceof Geometry) {
// for geometries, special case as we need to handle the CRS and we basically
// have no promotions, either all equal, or all Geometry.class
if (target == null) {
Geometry g = (Geometry) value;
AttributeTypeBuilder typeBuilder = new AttributeTypeBuilder();
typeBuilder.setName(name);
typeBuilder.setBinding(value.getClass());
if (g.getUserData() instanceof CoordinateReferenceSystem) {
typeBuilder.setCRS((CoordinateReferenceSystem) g.getUserData());
}
AttributeDescriptor geometryDescriptor = typeBuilder.buildDescriptor(name);
guessedTypes.put(name, geometryDescriptor);
} else if (Geometry.class.isAssignableFrom(target) && !target.isInstance(value)) {
AttributeTypeBuilder typeBuilder = new AttributeTypeBuilder();
typeBuilder.init(ad);
typeBuilder.setBinding(Geometry.class);
AttributeDescriptor geometryDescriptor = typeBuilder.buildDescriptor(name);
guessedTypes.put(name, geometryDescriptor);
}
} else {
Hints hints = new Hints(ConverterFactory.SAFE_CONVERSION, true);
if (target == null) {
for (Class c : TYPE_GUESS_TARGETS) {
Object converted = Converters.convert(value, c, hints);
if (converted != null) {
target = c;
break;
}
}
if (target == null) {
target = String.class;
}
}
// verify the current value is compatible with the target type
Object converted = Converters.convert(value, target, hints);
while (converted == null && TYPE_PROMOTIONS.get(target) != null) {
target = TYPE_PROMOTIONS.get(target);
converted = Converters.convert(value, target, hints);
}
// if all fails, use string
if (converted == null) {
target = String.class;
}
if (originalTarget != target) {
AttributeTypeBuilder typeBuilder = new AttributeTypeBuilder();
typeBuilder.setName(name);
typeBuilder.setBinding(target);
AttributeDescriptor newDescriptor = typeBuilder.buildDescriptor(name);
guessedTypes.put(name, newDescriptor);
}
}
}
@Override
public int getFeatureCount(ImportData data, ImportTask item) throws IOException {
return -1;
}
@Override
public String getName() {
return "GML";
}
@Override
public boolean canRead(ImportData data) throws IOException {
File file = getFileFromData(data);
return file.canRead() && "gml".equalsIgnoreCase(FilenameUtils.getExtension(file.getName()));
}
@Override
public StoreInfo createStore(ImportData data, WorkspaceInfo workspace, Catalog catalog)
throws IOException {
// no store support for GML
return null;
}
}