/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2009-2011, 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.memory; import com.vividsolutions.jts.geom.Geometry; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import org.apache.sis.feature.FeatureExt; import org.apache.sis.storage.DataStoreException; import org.geotoolkit.data.AbstractFeatureStore; import org.geotoolkit.data.FeatureStoreFactory; import org.geotoolkit.data.FeatureStoreRuntimeException; import org.geotoolkit.data.FeatureReader; import org.geotoolkit.data.FeatureWriter; import org.geotoolkit.data.query.DefaultQueryCapabilities; import org.geotoolkit.data.query.Query; import org.geotoolkit.data.query.QueryBuilder; import org.geotoolkit.data.query.QueryCapabilities; import org.geotoolkit.factory.FactoryFinder; import org.geotoolkit.factory.Hints; import org.geotoolkit.filter.identity.DefaultFeatureId; import org.geotoolkit.geometry.jts.JTS; import org.apache.sis.util.Utilities; import org.opengis.feature.Feature; import org.opengis.feature.FeatureType; import org.opengis.feature.PropertyType; import org.opengis.util.GenericName; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory; import org.opengis.filter.Id; import org.opengis.filter.identity.FeatureId; import org.opengis.filter.identity.Identifier; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.util.FactoryException; import org.apache.sis.internal.feature.AttributeConvention; import org.apache.sis.storage.IllegalNameException; import static org.apache.sis.util.ArgumentChecks.*; import org.geotoolkit.data.internal.GenericNameIndex; import org.opengis.feature.PropertyNotFoundException; /** * @todo : make this concurrent * @author Johann Sorel (Geomatys) * @module */ public class MemoryFeatureStore extends AbstractFeatureStore{ private static final FilterFactory FF = FactoryFinder.getFilterFactory(null); private static abstract class Group { final FeatureType type; final boolean hasIds; Group(final FeatureType type){ this.type = type; hasIds = hasIdentifier(type); } public FeatureType getFeatureType() { return type; } } private static class GroupWithId extends Group { final AtomicLong incId = new AtomicLong(-1);//first one will be 0 final Map<Object,Feature> features; GroupWithId(final FeatureType type){ super(type); this.features = new ConcurrentHashMap<>(); } public String generateId(){ while(true){ final long c = incId.incrementAndGet(); final String strc = new StringBuilder(getFeatureType().getName() .tip().toString()).append('.').append(c).toString(); if(!features.containsKey(strc)){ return strc; } } } public Iterator<? extends Feature> createIterator(final Id ids) { if(ids == null){ return features.values().iterator(); } final Set<Identifier> fids = ids.getIdentifiers(); final Iterator<Identifier> iteIds = fids.iterator(); return new Iterator<Feature>(){ Feature next = null; @Override public boolean hasNext() { findNext(); return next != null; } @Override public Feature next() { if(next == null){ throw new FeatureStoreRuntimeException("No more features."); } final Feature candidate = next; next = null; return candidate; } @Override public void remove() { throw new UnsupportedOperationException("Not supported."); } private void findNext(){ if(next != null) return; while(next == null && iteIds.hasNext()){ final String strid = iteIds.next().getID().toString(); next = features.get(strid); } } }; } } private static class GroupNoId extends Group { final List<Feature> features = new CopyOnWriteArrayList<>(); GroupNoId(final FeatureType type){ super(type); } public Iterator<? extends Feature> createIterator() { return features.iterator(); } } private final QueryCapabilities capabilities = new DefaultQueryCapabilities(false); private final boolean singleTypeLock; private final GenericNameIndex<Group> groups = new GenericNameIndex<>(); private Set<GenericName> nameCache = null; public MemoryFeatureStore(){ super(null); singleTypeLock = false; } /** * Memory feature store has no factory * @return null */ @Override public FeatureStoreFactory getFactory() { return null; } /** * Create a memory feature store with a single type. * * @param type * @param singleTypeLock : true if you don't want any other types to be create or * this type to be deleted. */ public MemoryFeatureStore(FeatureType type, final boolean singleTypeLock){ super(null); this.singleTypeLock = singleTypeLock; final GenericName name = type.getName(); try { groups.add(name, hasIdentifier(type) ? new GroupWithId(type) : new GroupNoId(type)); } catch (IllegalNameException ex) { //wont happen getLogger().log(Level.WARNING, ex.getMessage(), ex); } } /** * {@inheritDoc } */ @Override public synchronized Set<GenericName> getNames() throws DataStoreException { if(nameCache == null){ nameCache = groups.getNames(); } return nameCache; } /** * {@inheritDoc } */ @Override public FeatureType getFeatureType(final String name) throws DataStoreException { final Group grp = groups.get(name); if(grp == null){ throw new DataStoreException("Schema "+ name +" doesnt exist in this feature store."); } return grp.getFeatureType(); } /** * {@inheritDoc } */ @Override public synchronized void createFeatureType(final FeatureType type) throws DataStoreException { if(singleTypeLock) throw new DataStoreException( "Memory feature store is in single type mode. Schema modification are not allowed."); ensureNonNull("feature type", type); final GenericName name = type.getName(); if(groups.contains(name.toString())){ throw new IllegalArgumentException("FeatureType with name : " + type.getName() + " already exist."); } groups.add(name, hasIdentifier(type) ? new GroupWithId(type) : new GroupNoId(type)); //clear name cache nameCache = null; //fire event fireSchemaAdded(name, type); } /** * {@inheritDoc } */ @Override public synchronized void updateFeatureType(final FeatureType newType) throws DataStoreException { if(singleTypeLock) throw new DataStoreException( "Memory feature store is in single type mode. Schema modification are not allowed."); final GenericName typeName = newType.getName(); //todo must do it a way to avoid destroying all features. ensureNonNull("feature type", newType); ensureNonNull("name", typeName); final Group grp = groups.get(typeName.toString()); groups.remove(typeName); final FeatureType type = grp.getFeatureType(); groups.add(typeName, hasIdentifier(newType) ? new GroupWithId(newType) : new GroupNoId(newType)); //clear name cache nameCache = null; //fire update event fireSchemaUpdated(typeName, type, newType); } /** * {@inheritDoc } */ @Override public synchronized void deleteFeatureType(final String typeName) throws DataStoreException { if(singleTypeLock) throw new DataStoreException( "Memory feature store is in single type mode. Schema modification are not allowed."); final Group grp = groups.get(typeName); groups.remove(grp.type.getName()); //clear name cache nameCache = null; //fire event fireSchemaDeleted(grp.getFeatureType().getName(), grp.getFeatureType()); } /** * {@inheritDoc } */ @Override public QueryCapabilities getQueryCapabilities() { return capabilities; } /** * {@inheritDoc } */ @Override public List<FeatureId> addFeatures(final String groupName, final Collection<? extends Feature> collection, final Hints hints) throws DataStoreException { typeCheck(groupName); final Group grp = groups.get(groupName); final List<FeatureId> addedIds = new ArrayList<>(); for(final Feature f : collection){ Object candidateId = null; if(grp instanceof GroupWithId){ final GroupWithId grpwid = (GroupWithId) grp; candidateId = f.getPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString()); if(candidateId == null || "".equals(candidateId)){ //feature does not have an id, create one candidateId = grpwid.generateId(); }else{ Long test = null; if (candidateId instanceof Number) { test = ((Number) candidateId).longValue(); } if(test != null && test < 0){ //it's a decremented id value, we replace it candidateId = grpwid.generateId(); }else if(grpwid.features.containsKey(candidateId)){ //key already used, replace it candidateId = grpwid.generateId(); } } f.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), candidateId); addedIds.add(new DefaultFeatureId(String.valueOf(candidateId))); } //copy the feature final Feature copy = FeatureExt.copy(f); //force crs definition on each geometry for(PropertyType pt : copy.getType().getProperties(true)){ if(AttributeConvention.isGeometryAttribute(pt)){ CoordinateReferenceSystem crs = FeatureExt.getCRS(pt); if(crs==null) continue; Object value = copy.getPropertyValue(pt.getName().toString()); if(value instanceof Geometry){ try { CoordinateReferenceSystem geomCrs = JTS.findCoordinateReferenceSystem((Geometry) value); if(geomCrs!=null){ if(!Utilities.equalsIgnoreMetadata(geomCrs, crs)){ throw new DataStoreException("Geometry "+pt.getName().tip()+" CRS do not match FeatureType CRS"); } }else{ JTS.setCRS((Geometry)value, geomCrs); } } catch (FactoryException ex) { throw new DataStoreException(ex.getMessage(), ex); } } } } if(grp instanceof GroupWithId){ ((GroupWithId)grp).features.put(candidateId, copy); }else{ ((GroupNoId)grp).features.add(copy); } } //fire add event final Id eventIds = FF.id(new HashSet<Identifier>(addedIds)); fireFeaturesAdded(grp.type.getName(),eventIds); return addedIds; } /** * {@inheritDoc } */ @Override public void updateFeatures(final String groupName, final Filter filter, final Map<String, ?> values) throws DataStoreException { typeCheck(groupName); //get features which will be modified final Group grp = groups.get(groupName); //ensure crs is set on geometric values for(Map.Entry<String, ?> entry : values.entrySet()){ final String name = entry.getKey(); final Object value = entry.getValue(); //ensure the crs is set on the geometry if(value instanceof Geometry){ final PropertyType property = grp.getFeatureType().getProperty(name); final CoordinateReferenceSystem crs = FeatureExt.getCRS(property); if(crs==null) continue; final CoordinateReferenceSystem geomCrs; try { geomCrs = JTS.findCoordinateReferenceSystem((Geometry) value); if(geomCrs!=null){ if(!Utilities.equalsIgnoreMetadata(geomCrs, crs)){ throw new DataStoreException("Geometry "+property.getName().tip()+" CRS do not match FeatureType CRS"); } }else{ JTS.setCRS((Geometry)value, geomCrs); } } catch (FactoryException ex) { throw new DataStoreException(ex.getMessage(), ex); } } } if(grp instanceof GroupWithId){ final Collection<Identifier> toUpdate = getAffectedFeatures(groupName, filter); if(toUpdate.isEmpty()) return; final Set<Identifier> ups = new HashSet<>(); for(final Identifier itd : toUpdate){ final Feature candidate = ((GroupWithId)grp).features.get(itd.getID()); if(candidate == null) continue; ups.add(itd); for(Map.Entry<String, ?> entry : values.entrySet()){ final String name = entry.getKey(); final Object value = entry.getValue(); candidate.setPropertyValue(name, value); } } //fire update event final Id eventIds = FF.id(new HashSet<>(ups)); fireFeaturesUpdated(((GroupWithId) grp).type.getName(),eventIds); }else{ final GroupWithId grpnoid = (GroupWithId) grp; for (int i=grpnoid.features.size()-1;i>=0;i--) { Feature candidate = grpnoid.features.get(i); if (filter.evaluate(candidate)) { for(Map.Entry<String, ?> entry : values.entrySet()){ final String name = entry.getKey(); final Object value = entry.getValue(); candidate.setPropertyValue(name, value); } } } fireFeaturesUpdated(grpnoid.type.getName(),FF.id(new HashSet(Collections.EMPTY_SET))); } } /** * {@inheritDoc } */ @Override public void removeFeatures(final String groupName, final Filter filter) throws DataStoreException { typeCheck(groupName); final Group grp = groups.get(groupName); if(grp instanceof GroupWithId){ final GroupWithId grpwithid = (GroupWithId) grp; final Collection<Identifier> toRemove = getAffectedFeatures(groupName, filter); final Set<Identifier> rems = new HashSet<>(); for(final Identifier itd : toRemove){ final Feature candidate = grpwithid.features.remove(String.valueOf(itd.getID())); if(candidate == null) continue; rems.add(itd); } //fire remove event final Id eventIds = FF.id(new HashSet<>(rems)); fireFeaturesDeleted(grpwithid.type.getName(),eventIds); }else{ final GroupNoId grpnoid = (GroupNoId) grp; for (int i=grpnoid.features.size()-1;i>=0;i--) { Feature f = grpnoid.features.get(i); if (filter.evaluate(f)) { grpnoid.features.remove(i); } } fireFeaturesDeleted(grpnoid.type.getName(),FF.id(new HashSet(Collections.EMPTY_SET))); } } private Collection<Identifier> getAffectedFeatures(final String groupName, final Filter filter) throws DataStoreException{ final Group grp = groups.get(groupName); final Collection<Identifier> affected; if(filter instanceof Id){ final Id ids = (Id) filter; affected = ids.getIdentifiers(); }else{ affected = new ArrayList<>(); final QueryBuilder qb = new QueryBuilder(groupName); qb.setFilter(filter); qb.setProperties(new String[]{AttributeConvention.IDENTIFIER_PROPERTY.toString()}); //no properties, only ids final FeatureReader reader = getFeatureReader(qb.buildQuery()); try{ while(reader.hasNext()){ affected.add(FeatureExt.getId(reader.next())); } }finally{ reader.close(); } } return affected; } /** * {@inheritDoc } */ @Override public FeatureReader getFeatureReader(final Query query) throws DataStoreException { final Group grp = groups.get(query.getTypeName()); if(grp == null){ throw new DataStoreException("No featureType for name : " + query.getTypeName()); } //we can handle id filter final Filter filter = query.getFilter(); final QueryBuilder remaining = new QueryBuilder(query); final Iterator<? extends Feature> ite; if(grp instanceof GroupWithId){ if(filter instanceof Id){ ite = ((GroupWithId)grp).createIterator((Id)filter); if(ite != null){ remaining.setFilter(Filter.INCLUDE); } }else{ ite = ((GroupWithId)grp).createIterator(null); } }else{ ite = ((GroupNoId)grp).createIterator(); } final FeatureReader reader; reader = GenericWrapFeatureIterator.wrapToReader(ite, grp.getFeatureType()); //fall back on generic parameter handling. //todo we should handle at least spatial filter here by using a quadtree. return handleRemaining(reader, remaining.buildQuery()); } /** * {@inheritDoc } */ @Override public FeatureWriter getFeatureWriter(Query query) throws DataStoreException { return handleWriter(query); } /** * {@inheritDoc } */ @Override public void close() throws DataStoreException{ super.close(); groups.clear(); } @Override public void refreshMetaModel() { } private static boolean hasIdentifier(FeatureType type) { try{ type.getProperty(AttributeConvention.IDENTIFIER_PROPERTY.toString()); return true; }catch(PropertyNotFoundException ex){ return false; } } }