/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2014, Geomatys * * 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.geotoolkit.data.geojson; import org.geotoolkit.util.NamesExt; import org.opengis.util.GenericName; import com.vividsolutions.jts.geom.*; import org.apache.sis.storage.DataStoreException; import org.apache.sis.util.logging.Logging; import org.geotoolkit.data.*; import org.geotoolkit.data.geojson.binding.*; import org.geotoolkit.data.geojson.utils.FeatureTypeUtils; import org.geotoolkit.data.geojson.utils.GeoJSONParser; import org.geotoolkit.data.geojson.utils.GeoJSONUtils; import org.geotoolkit.data.query.*; import org.geotoolkit.factory.Hints; import org.geotoolkit.factory.HintsPending; import org.geotoolkit.parameter.Parameters; import org.opengis.filter.Filter; import org.opengis.filter.identity.FeatureId; import org.opengis.geometry.Envelope; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.crs.CoordinateReferenceSystem; import static org.geotoolkit.data.geojson.GeoJSONFeatureStoreFactory.*; import static org.geotoolkit.data.geojson.binding.GeoJSONGeometry.*; import java.io.IOException; import java.net.URI; import java.nio.file.FileSystemNotFoundException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.sis.feature.builder.AttributeRole; import org.apache.sis.feature.builder.AttributeTypeBuilder; import org.apache.sis.feature.builder.FeatureTypeBuilder; import org.apache.sis.internal.feature.AttributeConvention; import org.geotoolkit.storage.DataStores; import org.opengis.feature.Feature; import org.opengis.feature.FeatureType; import org.opengis.feature.PropertyNotFoundException; /** * * @author Quentin Boileau (Geomatys) */ public class GeoJSONFeatureStore extends AbstractFeatureStore { private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.data.geojson"); private static final String DESC_FILE_SUFFIX = "_Type.json"; private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); private final ReadWriteLock tmpLock = new ReentrantReadWriteLock(); private final QueryCapabilities capabilities = new DefaultQueryCapabilities(false, false); private GenericName name; private FeatureType featureType; private Path descFile; private Path jsonFile; private Integer coordAccuracy; private boolean isLocal = true; public GeoJSONFeatureStore(final Path path, final String namespace, Integer coordAccuracy) throws DataStoreException { this(toParameter(path.toUri(), namespace, coordAccuracy)); } public GeoJSONFeatureStore(final URI uri, final String namespace, Integer coordAccuracy) throws DataStoreException { this(toParameter(uri, namespace, coordAccuracy)); } public GeoJSONFeatureStore (final ParameterValueGroup params) throws DataStoreException { super(params); this.coordAccuracy = (Integer) params.parameter(COORDINATE_ACCURACY.getName().toString()).getValue(); final URI uri = (URI) params.parameter(PATH.getName().toString()).getValue(); //FIXME this.isLocal = "file".equalsIgnoreCase(uri.getScheme()); Path tmpFile = null; try { tmpFile = Paths.get(uri); } catch (FileSystemNotFoundException ex) { throw new DataStoreException(ex); } final String fileName = tmpFile.getFileName().toString(); if (fileName.endsWith(DESC_FILE_SUFFIX)) { this.descFile = tmpFile; this.jsonFile = descFile.resolveSibling(fileName.replace(DESC_FILE_SUFFIX, ".json")); } else { this.jsonFile = tmpFile; //search for description json file String typeName = GeoJSONUtils.getNameWithoutExt(jsonFile); this.descFile = jsonFile.resolveSibling(typeName + DESC_FILE_SUFFIX); } } private static ParameterValueGroup toParameter(final URI uri, final String namespace, Integer coordAccuracy){ final ParameterValueGroup params = GeoJSONFeatureStoreFactory.PARAMETERS_DESCRIPTOR.createValue(); Parameters.getOrCreate(GeoJSONFeatureStoreFactory.PATH, params).setValue(uri); Parameters.getOrCreate(GeoJSONFeatureStoreFactory.NAMESPACE, params).setValue(namespace); Parameters.getOrCreate(GeoJSONFeatureStoreFactory.COORDINATE_ACCURACY, params).setValue(coordAccuracy); return params; } @Override public FeatureStoreFactory getFactory() { return (FeatureStoreFactory) DataStores.getFactoryById(GeoJSONFeatureStoreFactory.NAME); } @Override public boolean isWritable(final String typeName) throws DataStoreException { return isLocal && Files.isWritable(descFile) && Files.isWritable(jsonFile); } public GenericName getName() throws DataStoreException{ checkTypeExist(); return name; } public FeatureType getFeatureType() throws DataStoreException{ checkTypeExist(); return featureType; } private void checkTypeExist() throws DataStoreException { if (name != null && featureType != null) { return; } else { try { // try to parse file only if exist and not empty if (Files.exists(jsonFile) && Files.size(jsonFile) != 0) { featureType = readType(); name = featureType.getName(); } } catch (IOException e) { LOGGER.log(Level.WARNING, e.getMessage(), e); } } } /** * Read FeatureType from a JSON-Schema file if exist or directly from the input JSON file. * @return * @throws DataStoreException * @throws IOException */ private FeatureType readType() throws DataStoreException, IOException { if (Files.exists(descFile) && Files.size(descFile) != 0) { // build FeatureType from description JSON. return FeatureTypeUtils.readFeatureType(descFile); } else { if(Files.exists(jsonFile) && Files.size(jsonFile) != 0) { final String name = GeoJSONUtils.getNameWithoutExt(jsonFile); final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); ftb.setName(name); // build FeatureType from the first Feature of JSON file. final GeoJSONObject obj = GeoJSONParser.parse(jsonFile, true); if (obj == null) { throw new DataStoreException("Invalid GeoJSON file " + jsonFile.toString()); } CoordinateReferenceSystem crs = GeoJSONUtils.getCRS(obj); if (obj instanceof GeoJSONFeatureCollection) { GeoJSONFeatureCollection jsonFeatureCollection = (GeoJSONFeatureCollection) obj; if (!jsonFeatureCollection.hasNext()) { //empty FeatureCollection error ? throw new DataStoreException("Empty GeoJSON FeatureCollection " + jsonFile.toString()); } else { // TODO should we analyse all Features from FeatureCollection to be sure // that each Feature properties JSON object define exactly the same properties // with the same bindings ? GeoJSONFeature jsonFeature = jsonFeatureCollection.next(); fillTypeFromFeature(ftb, crs, jsonFeature, false); } } else if (obj instanceof GeoJSONFeature) { GeoJSONFeature jsonFeature = (GeoJSONFeature) obj; fillTypeFromFeature(ftb, crs, jsonFeature, true); } else if (obj instanceof GeoJSONGeometry) { HashMap<Object, Object> userData = new HashMap<>(); userData.put(HintsPending.PROPERTY_IS_IDENTIFIER,Boolean.TRUE); ftb.addAttribute(String.class).setName("fid").addRole(AttributeRole.IDENTIFIER_COMPONENT); ftb.addAttribute(findBinding((GeoJSONGeometry) obj)).setName("geometry").setCRS(crs).addRole(AttributeRole.DEFAULT_GEOMETRY); } try{ ftb.build().getProperty(AttributeConvention.IDENTIFIER_PROPERTY.toString()); }catch(PropertyNotFoundException ex){ ftb.addAttribute(String.class).setName(AttributeConvention.IDENTIFIER_PROPERTY); } return ftb.build(); } else { throw new DataStoreException("Can't create FeatureType from empty/not found Json file "+jsonFile.getFileName().toString()); } } } private void fillTypeFromFeature(FeatureTypeBuilder ftb, CoordinateReferenceSystem crs, GeoJSONFeature jsonFeature, boolean analyseGeometry) { if (analyseGeometry) { ftb.addAttribute(findBinding(jsonFeature.getGeometry())).setName("geometry").setCRS(crs).addRole(AttributeRole.DEFAULT_GEOMETRY); } else { ftb.addAttribute(Geometry.class).setName("geometry").setCRS(crs).addRole(AttributeRole.DEFAULT_GEOMETRY); } for (Map.Entry<String, Object> property : jsonFeature.getProperties().entrySet()) { final Object value = property.getValue(); final Class binding = value != null ? value.getClass() : String.class; final GenericName name = NamesExt.create(property.getKey()); final AttributeTypeBuilder atb = ftb.addAttribute(binding).setName(name); if ("id".equals(property.getKey()) || "fid".equals(property.getKey())) { atb.addRole(AttributeRole.IDENTIFIER_COMPONENT); } } } private Class findBinding(GeoJSONGeometry jsonGeometry) { if (jsonGeometry instanceof GeoJSONPoint) { return Point.class; } else if (jsonGeometry instanceof GeoJSONLineString) { return LineString.class; } else if (jsonGeometry instanceof GeoJSONPolygon) { return Polygon.class; } else if (jsonGeometry instanceof GeoJSONMultiPoint) { return MultiPoint.class; } else if (jsonGeometry instanceof GeoJSONMultiLineString) { return MultiLineString.class; } else if (jsonGeometry instanceof GeoJSONMultiPolygon) { return MultiPolygon.class; } else if (jsonGeometry instanceof GeoJSONGeometryCollection) { return GeometryCollection.class; } else { throw new IllegalArgumentException("Unsupported geometry type : " + jsonGeometry); } } private void writeType(FeatureType featureType) throws DataStoreException { try { final boolean jsonExist = Files.exists(jsonFile); if (jsonExist && Files.size(jsonFile) != 0) { throw new DataStoreException(String.format("Non empty json file %s can't create new json file %s", jsonFile.getFileName().toString(), featureType.getName())); } if (!jsonExist) Files.createFile(jsonFile); //create json with empty collection GeoJSONUtils.writeEmptyFeatureCollection(jsonFile); //json schema file final boolean descExist = Files.exists(descFile); if (descExist && Files.size(descFile) != 0) { throw new DataStoreException(String.format("Non empty json schema file %s can't create new json schema %s", descFile.getFileName().toString(), featureType.getName())); } if (!descExist) Files.createFile(descFile); //create json schema file FeatureTypeUtils.writeFeatureType(featureType, descFile); this.featureType = featureType; this.name = featureType.getName(); } catch (IOException e) { throw new DataStoreException(e.getMessage(), e); } } /** * {@inheritDoc } */ @Override public Set<GenericName> getNames() throws DataStoreException { GenericName name = getName(); if (name != null) { return Collections.singleton(getName()); } else { return Collections.EMPTY_SET; } } /** * {@inheritDoc } */ @Override public QueryCapabilities getQueryCapabilities() { return capabilities; } @Override public Envelope getEnvelope(final Query query) throws DataStoreException, FeatureStoreRuntimeException { typeCheck(query.getTypeName()); if(QueryUtilities.queryAll(query)){ try { rwLock.readLock().lock(); final GeoJSONObject obj = GeoJSONParser.parse(jsonFile, true); final CoordinateReferenceSystem crs = GeoJSONUtils.getCRS(obj); final Envelope envelope = GeoJSONUtils.getEnvelope(obj, crs); if (envelope != null) { return envelope; } rwLock.readLock().unlock(); } catch (IOException e) { throw new DataStoreException(e.getMessage(), e); } } //fallback return super.getEnvelope(query); } /** * {@inheritDoc } */ @Override public FeatureReader getFeatureReader(final Query query) throws DataStoreException { typeCheck(query.getTypeName()); final FeatureReader fr = new GeoJSONReader(jsonFile, featureType, rwLock); return handleRemaining(fr, query); } /** * {@inheritDoc } */ @Override public FeatureWriter getFeatureWriter(Query query) throws DataStoreException { typeCheck(query.getTypeName()); final FeatureWriter fw = new GeoJSONFileWriter(jsonFile, featureType, rwLock, tmpLock, GeoJSONFeatureStoreFactory.ENCODING, coordAccuracy); return handleRemaining(fw, query.getFilter()); } //////////////////////////////////////////////////////////////////////////// // FeatureType manipulation ///////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /** * {@inheritDoc } */ @Override public FeatureType getFeatureType(final String typeName) throws DataStoreException { checkTypeExist(); if (featureType == null) { throw new DataStoreException("No FeatureType found for type name : "+typeName); } return featureType; } @Override public void createFeatureType(final FeatureType featureType) throws DataStoreException { if (!isLocal) { throw new DataStoreException("Cannot create FeatureType on remote GeoJSON"); } GenericName typeName = featureType.getName(); if(typeName == null){ throw new DataStoreException("Type name can not be null."); } if(this.featureType != null){ throw new DataStoreException("Can only have one feature type in GeoJSON dataStore."); } if (!typeName.tip().toString().equals(GeoJSONUtils.getNameWithoutExt(jsonFile))) { throw new DataStoreException("New type name should be equals to file name."); } try{ rwLock.writeLock().lock(); writeType(featureType); }finally{ rwLock.writeLock().unlock(); } fireSchemaAdded(typeName, featureType); } /** * {@inheritDoc } */ @Override public void updateFeatureType(final FeatureType featureType) throws DataStoreException { deleteFeatureType(featureType.getName().toString()); createFeatureType(featureType); } /** * {@inheritDoc } */ @Override public void deleteFeatureType(final String typeName) throws DataStoreException { typeCheck(typeName); if (!isLocal) { throw new DataStoreException("Cannot create FeatureType on remote GeoJSON"); } if(typeName == null){ throw new DataStoreException("Type name can not be null."); } if (!typeName.equals(GeoJSONUtils.getNameWithoutExt(jsonFile))) { throw new DataStoreException("New type name should be equals to file name."); } try{ rwLock.writeLock().lock(); Files.deleteIfExists(descFile); Files.deleteIfExists(jsonFile); Files.createFile(jsonFile); } catch (IOException e) { throw new DataStoreException("Can not delete GeoJSON schema.", e); } finally{ rwLock.writeLock().unlock(); } } //////////////////////////////////////////////////////////////////////////// //Fallback on iterative reader and writer ////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /** * {@inheritDoc } */ @Override public List<FeatureId> addFeatures(final String groupName, final Collection<? extends Feature> newFeatures, final Hints hints) throws DataStoreException { return handleAddWithFeatureWriter(groupName, newFeatures, hints); } /** * {@inheritDoc } */ @Override public void updateFeatures(final String groupName, final Filter filter, final Map<String, ? extends Object> values) throws DataStoreException { handleUpdateWithFeatureWriter(groupName, filter, values); } /** * {@inheritDoc } */ @Override public void removeFeatures(final String groupName, final Filter filter) throws DataStoreException { handleRemoveWithFeatureWriter(groupName, filter); } @Override public void refreshMetaModel() { name = null; featureType = null; } }