/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2010, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.geojson.feature;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geojson.DelegatingHandler;
import org.geotools.geojson.IContentHandler;
import org.json.simple.parser.ParseException;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
/**
* Obtains a complete feature type from GeoJSON by parsing beyond first feature
* and finding attributes that did not appear in the first feature or had null
* values.
*
* If null values are encoded, parsing will stop when all data types are found.
* In the worst case, all features will be parsed. If null values are not
* encoded, all features will be parsed anyway.
*
*/
public class FeatureTypeHandler extends DelegatingHandler<SimpleFeatureType>
implements IContentHandler<SimpleFeatureType> {
SimpleFeatureType featureType;
private boolean inFeatures = false;
private Map<String, Class<?>> propertyTypes = new LinkedHashMap<String, Class<?>>();
private boolean inProperties;
private String currentProp;
private CoordinateReferenceSystem crs;
private boolean nullValuesEncoded;
private GeometryDescriptor geom;
public FeatureTypeHandler(boolean nullValuesEncoded) {
this.nullValuesEncoded = nullValuesEncoded;
}
@Override
public boolean startObjectEntry(String key) throws ParseException,
IOException {
if ("crs".equals(key)) {
delegate = new CRSHandler();
return true;
}
if ("features".equals(key)) {
delegate = UNINITIALIZED;
inFeatures = true;
return true;
}
if (inFeatures && delegate == NULL) {
if ("properties".equals(key)) {
inProperties = true;
return true;
}
if (inProperties) {
if (!propertyTypes.containsKey(key)) {
// found previously unknown property
propertyTypes.put(key, Object.class);
}
currentProp = key;
return true;
}
}
return super.startObjectEntry(key);
}
@Override
public boolean startArray() throws ParseException, IOException {
/*
* Use FeatureHandler for the first feature only, to initialize the property
* list and obtain the geometry attribute descriptor
*/
if (delegate == UNINITIALIZED) {
delegate = new FeatureHandler(null, new DefaultAttributeIO());
return true;
}
return super.startArray();
}
@Override
public boolean endObject() throws ParseException, IOException {
super.endObject();
if (delegate instanceof FeatureHandler) {
// obtain a type from the first feature
SimpleFeature feature = ((FeatureHandler) delegate).getValue();
if (feature != null) {
geom = feature.getFeatureType().getGeometryDescriptor();
List<AttributeDescriptor> attributeDescriptors = feature
.getFeatureType().getAttributeDescriptors();
for (AttributeDescriptor ad : attributeDescriptors) {
if (!ad.equals(geom)) {
propertyTypes.put(ad.getLocalName(), ad.getType().getBinding());
}
}
delegate = NULL;
if (foundAllValues()) {
buildType();
return false;
}
}
}
return true;
}
@Override
public boolean primitive(Object value) throws ParseException, IOException {
if (value != null) {
Class<?> newType = value.getClass();
if (currentProp != null) {
Class<?> knownType = propertyTypes.get(currentProp);
if (knownType == Object.class) {
propertyTypes.put(currentProp, newType);
if (foundAllValues()) {
// found the last unknown type, stop parsing
buildType();
return false;
}
} else if (knownType != newType) {
if (Number.class.isAssignableFrom(knownType) && newType == Double.class) {
propertyTypes.put(currentProp, Double.class);
} else {
throw new IllegalStateException("Found conflicting types " + knownType.getSimpleName() + " and "
+ newType.getSimpleName() + " for property " + currentProp);
}
}
}
}
return super.primitive(value);
}
/*
* When null values are encoded there's the possibility of stopping the
* parsing earlier, i.e.: as soon as all data types and the crs are found.
*/
private boolean foundAllValues() {
return nullValuesEncoded && geom != null && crs != null
&& !thereAreUnknownDataTypes();
}
private boolean thereAreUnknownDataTypes() {
for (Class<?> clazz : propertyTypes.values()) {
if (clazz == Object.class) {
return true;
}
}
return false;
}
@Override
public boolean endObjectEntry() throws ParseException, IOException {
super.endObjectEntry();
if (delegate != null && delegate instanceof CRSHandler) {
crs = ((CRSHandler) delegate).getValue();
if (crs != null) {
delegate = NULL;
}
} else if (currentProp != null) {
currentProp = null;
} else if (inProperties) {
inProperties = false;
}
return true;
}
@Override
public void endJSON() throws ParseException, IOException {
buildType();
}
private void buildType() {
SimpleFeatureTypeBuilder typeBuilder = new SimpleFeatureTypeBuilder();
typeBuilder.setName("feature");
typeBuilder.setNamespaceURI("http://geotools.org");
if (geom != null) {
typeBuilder.add(geom.getLocalName(), geom.getType().getBinding(), crs);
}
if (propertyTypes != null) {
Set<Entry<String, Class<?>>> entrySet = propertyTypes.entrySet();
for (Entry<String, Class<?>> entry : entrySet) {
Class<?> binding = entry.getValue();
if (binding.equals(Object.class)) {
binding = String.class;
}
typeBuilder.add(entry.getKey(), binding);
}
}
if (crs != null) {
typeBuilder.setCRS(crs);
}
featureType = typeBuilder.buildFeatureType();
}
@Override
public SimpleFeatureType getValue() {
return featureType;
}
}