/* * This is part of Geomajas, a GIS framework, http://www.geomajas.org/. * * Copyright 2008-2016 Geosparc nv, http://www.geosparc.com/, Belgium. * * The program is available in open source according to the GNU Affero * General Public License. All contributions in this program are covered * by the Geomajas Contributors License Agreement. For full licensing * details, see LICENSE.txt in the project root. */ package org.geomajas.layer.geotools; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.annotation.PostConstruct; import javax.sql.DataSource; import org.geomajas.annotation.Api; import org.geomajas.configuration.Parameter; import org.geomajas.configuration.VectorLayerInfo; import org.geomajas.global.ExceptionCode; import org.geomajas.layer.LayerException; import org.geomajas.layer.VectorLayer; import org.geomajas.layer.feature.Attribute; import org.geomajas.layer.feature.FeatureModel; import org.geomajas.layer.shapeinmem.FeatureSourceRetriever; import org.geomajas.service.DtoConverterService; import org.geomajas.service.FilterService; import org.geomajas.service.GeoService; import org.geotools.data.DataStore; import org.geotools.data.FeatureSource; import org.geotools.data.FeatureStore; import org.geotools.data.Query; import org.geotools.data.shapefile.ShapefileDataStoreFactory; import org.geotools.data.simple.SimpleFeatureSource; import org.geotools.data.simple.SimpleFeatureStore; import org.geotools.feature.DefaultFeatureCollection; import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureIterator; import org.geotools.jdbc.JDBCDataStoreFactory; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.Name; import org.opengis.filter.Filter; import org.opengis.filter.identity.FeatureId; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import com.vividsolutions.jts.geom.Envelope; /** * GeoTools layer model. * * @author Jan De Moerloose * @author Pieter De Graef * @author Joachim Van der Auwera * @author Kristof Heirwegh * @since 1.7.1 */ @Api public class GeoToolsLayer extends FeatureSourceRetriever implements VectorLayer { private final Logger log = LoggerFactory.getLogger(GeoToolsLayer.class); private static final long DEFAULT_COOLDOWN_TIME = 60000; // millis // WARNING this may change when using a different GeoTools library version private static final String MAGIC_STRING_LIBRARY_MISSING = "No datastore found. Possible causes are " + "missing factory or missing library for your datastore (e.g. database driver)."; private FeatureModel featureModel; private VectorLayerInfo layerInfo; private String url; private String dbtype; private List<Parameter> parameters; private DataSource dataSource; @Autowired private FilterService filterService; @Autowired private GeoService geoService; @Autowired private DtoConverterService converterService; @Autowired private GeoToolsTransactionSynchronization transactionSynchronization; private CoordinateReferenceSystem crs; private String id; private boolean featureModelUsable; private long lastInitFeaturesRetry; private long cooldownTimeBetweenInitializationRetries = DEFAULT_COOLDOWN_TIME; @Override public String getId() { return id; } /** * Set the id for this layer. * * @param id layer id * @since 1.8.0 */ @Api public void setId(String id) { this.id = id; } /** * Set the URL for the case when the data source is a shape file. * <p/> * You cal also use "classpath" as protocol if the shape file is stored in the classpath. * <p/> * Important note: the shape file data store (specifically the indexing code) is not thread safe, so it should not * be used for writing. * * @param url shape file url * @since 1.8.0 */ @Api public void setUrl(String url) { this.url = url; } /** * Set database type. Useful when the data store is a database. * * @param dbtype database type * @since 1.8.0 */ @Api public void setDbtype(String dbtype) { this.dbtype = dbtype; } /** * Get the data source used by this layer (optional and only for database layers). This is the data source that is * passed to the GeoTools data store. * * @return the data source or null if not set * @since 1.10.0 */ @Api public DataSource getDataSource() { return dataSource; } /** * Set the data source used by this layer. This optional property can be used to bind a layers to an external data * source. Alternatively, one can pass the JNDI name of a data source as a normal parameter. * * @param dataSource the data source * @since 1.10.0 */ @Api public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } /** * Set additional parameters for the GeoTools data store. * * @param parameters parameter list * @since 1.8.0 */ @Api public void setParameters(List<Parameter> parameters) { this.parameters = parameters; } /** * The time to wait between initialization retries in case the service is unavailable. * * @param cooldownTimeBetweenInitializationRetries cool down time in milliseconds * @since 1.8.0 */ @Api public void setCooldownTimeBetweenInitializationRetries(long cooldownTimeBetweenInitializationRetries) { this.cooldownTimeBetweenInitializationRetries = cooldownTimeBetweenInitializationRetries; } @Override public CoordinateReferenceSystem getCrs() { return crs; } /** * Set the layer configuration. * * @param layerInfo layer information * @throws LayerException oops * @since 1.7.1 */ @Api public void setLayerInfo(VectorLayerInfo layerInfo) throws LayerException { super.setLayerInfo(layerInfo); this.layerInfo = layerInfo; } @Override public VectorLayerInfo getLayerInfo() { return layerInfo; } @Override public boolean isCreateCapable() { return true; } @Override public boolean isUpdateCapable() { return true; } @Override public boolean isDeleteCapable() { return true; } /** * {@inheritDoc} * * @deprecated set the data store parameters on the parameter object instead */ @Override @Deprecated public void setDataStore(DataStore dataStore) throws LayerException { super.setDataStore(dataStore); } /** * Finish initializing the layer. * * @throws LayerException oops */ @PostConstruct protected void initFeatures() throws LayerException { if (null == layerInfo) { return; } crs = geoService.getCrs2(layerInfo.getCrs()); setFeatureSourceName(layerInfo.getFeatureInfo().getDataSourceName()); try { if (null == super.getDataStore()) { Map<String, Object> params = new HashMap<String, Object>(); if (null != url) { params.put(ShapefileDataStoreFactory.URLP.key, url); } if (null != dbtype) { params.put(JDBCDataStoreFactory.DBTYPE.key, dbtype); } if (null != parameters) { for (Parameter parameter : parameters) { params.put(parameter.getName(), parameter.getValue()); } } if (null != dataSource) { params.put(JDBCDataStoreFactory.DATASOURCE.key, dataSource); // these are apparently required but not used params.put(JDBCDataStoreFactory.DATABASE.key, "some_database"); params.put(JDBCDataStoreFactory.USER.key, "some_user"); params.put(JDBCDataStoreFactory.PASSWD.key, "some_password"); params.put(JDBCDataStoreFactory.HOST.key, "some host"); params.put(JDBCDataStoreFactory.PORT.key, "0"); } DataStore store = DataStoreFactory.create(params); super.setDataStore(store); } if (null == super.getDataStore()) { return; } this.featureModel = new GeoToolsFeatureModel(super.getDataStore(), layerInfo.getFeatureInfo() .getDataSourceName(), geoService.getSridFromCrs(layerInfo.getCrs()), converterService); featureModel.setLayerInfo(layerInfo); featureModelUsable = true; } catch (IOException ioe) { if (MAGIC_STRING_LIBRARY_MISSING.equals(ioe.getMessage())) { throw new LayerException(ioe, ExceptionCode.LAYER_MODEL_IO_EXCEPTION, url); } else { featureModelUsable = false; log.warn("The layer could not be correctly initialized: " + getId(), ioe); } } catch (LayerException le) { featureModelUsable = false; log.warn("The layer could not be correctly initialized: " + getId(), le); } catch (RuntimeException e) { featureModelUsable = false; log.warn("The layer could not be correctly initialized: " + getId(), e); } } @Override @Transactional(rollbackFor = { Throwable.class }) public Object create(Object feature) throws LayerException { SimpleFeatureSource source = getFeatureSource(); if (source instanceof SimpleFeatureStore) { SimpleFeatureStore store = (SimpleFeatureStore) source; DefaultFeatureCollection collection = new DefaultFeatureCollection(); collection.add((SimpleFeature) feature); transactionSynchronization.synchTransaction(store); try { List<FeatureId> ids = store.addFeatures(collection); // fetch it again to get the generated values !!! if (ids.size() == 1) { return read(ids.get(0).getID()); } } catch (IOException ioe) { featureModelUsable = false; throw new LayerException(ioe, ExceptionCode.LAYER_MODEL_IO_EXCEPTION); } return feature; } else { log.error("Don't know how to create or update " + getFeatureSourceName() + ", class " + source.getClass().getName() + " does not implement SimpleFeatureStore"); throw new LayerException(ExceptionCode.CREATE_OR_UPDATE_NOT_IMPLEMENTED, getFeatureSourceName(), source .getClass().getName()); } } /** * Update an existing feature. Made package private for testing purposes. * * @param feature feature to update * @throws LayerException oops */ void update(Object feature) throws LayerException { SimpleFeatureSource source = getFeatureSource(); if (source instanceof SimpleFeatureStore) { SimpleFeatureStore store = (SimpleFeatureStore) source; String featureId = getFeatureModel().getId(feature); Filter filter = filterService.createFidFilter(new String[] { featureId }); transactionSynchronization.synchTransaction(store); List<Name> names = new ArrayList<Name>(); Map<String, Attribute> attrMap = getFeatureModel().getAttributes(feature); List<Object> values = new ArrayList<Object>(); for (Map.Entry<String, Attribute> entry : attrMap.entrySet()) { String name = entry.getKey(); names.add(store.getSchema().getDescriptor(name).getName()); values.add(entry.getValue().getValue()); } try { store.modifyFeatures(names.toArray(new Name[names.size()]), values.toArray(), filter); store.modifyFeatures(store.getSchema().getGeometryDescriptor().getName(), getFeatureModel() .getGeometry(feature), filter); log.debug("Updated feature {} in {}", featureId, getFeatureSourceName()); } catch (IOException ioe) { featureModelUsable = false; throw new LayerException(ioe, ExceptionCode.LAYER_MODEL_IO_EXCEPTION); } } else { log.error("Don't know how to create or update " + getFeatureSourceName() + ", class " + source.getClass().getName() + " does not implement SimpleFeatureStore"); throw new LayerException(ExceptionCode.CREATE_OR_UPDATE_NOT_IMPLEMENTED, getFeatureSourceName(), source .getClass().getName()); } } @Override @Transactional(rollbackFor = { Throwable.class }) public void delete(String featureId) throws LayerException { SimpleFeatureSource source = getFeatureSource(); if (source instanceof SimpleFeatureStore) { SimpleFeatureStore store = (SimpleFeatureStore) source; Filter filter = filterService.createFidFilter(new String[] { featureId }); transactionSynchronization.synchTransaction(store); try { store.removeFeatures(filter); if (log.isDebugEnabled()) { log.debug("Deleted feature {} in {}", featureId, getFeatureSourceName()); } } catch (IOException ioe) { featureModelUsable = false; throw new LayerException(ioe, ExceptionCode.LAYER_MODEL_IO_EXCEPTION); } } else { log.error("Don't know how to delete from " + getFeatureSourceName() + ", class " + source.getClass().getName() + " does not implement SimpleFeatureStore"); throw new LayerException(ExceptionCode.DELETE_NOT_IMPLEMENTED, getFeatureSourceName(), source.getClass() .getName()); } } @Override @Transactional(rollbackFor = { Throwable.class }) public Object saveOrUpdate(Object feature) throws LayerException { if (exists(getFeatureModel().getId(feature))) { update(feature); } else { feature = create(feature); } return feature; } @Override public Object read(String featureId) throws LayerException { Filter filter = filterService.createFidFilter(new String[] { featureId }); Iterator<?> iterator = getElements(filter, 0, 0); if (iterator.hasNext()) { return iterator.next(); } else { throw new LayerException(ExceptionCode.LAYER_MODEL_FEATURE_NOT_FOUND, featureId); } } @Override public Envelope getBounds() throws LayerException { return getBounds(null); } @Override public Envelope getBounds(Filter filter) throws LayerException { FeatureSource<SimpleFeatureType, SimpleFeature> source = getFeatureSource(); if (source instanceof FeatureStore<?, ?>) { SimpleFeatureStore store = (SimpleFeatureStore) source; transactionSynchronization.synchTransaction(store); } try { FeatureCollection<SimpleFeatureType, SimpleFeature> fc; if (null == filter) { fc = source.getFeatures(); } else { fc = source.getFeatures(filter); } FeatureIterator<SimpleFeature> it = fc.features(); transactionSynchronization.addIterator(it); return fc.getBounds(); } catch (Throwable t) { // NOSONAR avoid errors (like NPE) as well throw new LayerException(t, ExceptionCode.UNEXPECTED_PROBLEM); } } /** * {@inheritDoc} * */ @Transactional(readOnly = true) public Iterator<?> getElements(Filter filter, int offset, int maxResultSize) throws LayerException { FeatureSource<SimpleFeatureType, SimpleFeature> source = getFeatureSource(); try { if (source instanceof FeatureStore<?, ?>) { SimpleFeatureStore store = (SimpleFeatureStore) source; transactionSynchronization.synchTransaction(store); } Query query = new Query(); query.setFilter(filter); query.setMaxFeatures(maxResultSize > 0 ? maxResultSize : Integer.MAX_VALUE); query.setStartIndex(offset); FeatureCollection<SimpleFeatureType, SimpleFeature> fc = source.getFeatures(query); FeatureIterator<SimpleFeature> it = fc.features(); transactionSynchronization.addIterator(it); return new JavaIterator(it); } catch (Throwable t) { // NOSONAR avoid errors (like NPE) as well throw new LayerException(t, ExceptionCode.UNEXPECTED_PROBLEM); } } @Override public FeatureModel getFeatureModel() { if (!featureModelUsable) { retryInitFeatures(); } return this.featureModel; } private boolean exists(String featureId) throws LayerException { Filter filter = filterService.createFidFilter(new String[] { featureId }); Iterator<?> iterator = getElements(filter, 0, 0); return iterator.hasNext(); } /** * Adapter to java iterator. * * @author Jan De Moerloose */ private static class JavaIterator implements Iterator<SimpleFeature> { private final FeatureIterator<SimpleFeature> delegate; public JavaIterator(FeatureIterator<SimpleFeature> delegate) { this.delegate = delegate; } @Override public boolean hasNext() { return delegate.hasNext(); } @Override public SimpleFeature next() { return delegate.next(); } @Override public void remove() { throw new UnsupportedOperationException("Feature removal not supported, use delete(id) instead"); } } @Override public DataStore getDataStore() { if (!featureModelUsable) { retryInitFeatures(); } return super.getDataStore(); } private void retryInitFeatures() { // do not hammer the service long now = System.currentTimeMillis(); if (now > lastInitFeaturesRetry + cooldownTimeBetweenInitializationRetries) { lastInitFeaturesRetry = now; try { log.debug("Retrying to (re-)initialize layer {}", getId()); initFeatures(); } catch (Exception e) { // NOSONAR log.warn("Failed initializing layer: ", e.getMessage()); } } } @Override public SimpleFeatureSource getFeatureSource() throws LayerException { if (!featureModelUsable) { retryInitFeatures(); } return super.getFeatureSource(); } }