/*
* This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
*
* Copyright 2008-2011 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 org.geomajas.configuration.Parameter;
import org.geomajas.configuration.VectorLayerInfo;
import org.geomajas.global.Api;
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.DataUtilities;
import org.geotools.data.FeatureSource;
import org.geotools.data.FeatureStore;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.data.simple.SimpleFeatureStore;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
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.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
@Transactional(rollbackFor = { Exception.class })
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;
@Autowired
private FilterService filterService;
@Autowired
private GeoService geoService;
@Autowired(required = false)
private GeoToolsTransactionManager transactionManager;
@Autowired
private DtoConverterService converterService;
private CoordinateReferenceSystem crs;
private String id;
private boolean featureModelUsable;
private long lastInitFeaturesRetry;
private long cooldownTimeBetweenInitializationRetries = DEFAULT_COOLDOWN_TIME;
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;
}
/**
* 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 cooldown time in milliseconds
* @since 1.8.0
*/
@Api
public void setCooldownTimeBetweenInitializationRetries(long cooldownTimeBetweenInitializationRetries) {
this.cooldownTimeBetweenInitializationRetries = cooldownTimeBetweenInitializationRetries;
}
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 {
this.layerInfo = layerInfo;
}
public VectorLayerInfo getLayerInfo() {
return layerInfo;
}
public boolean isCreateCapable() {
return true;
}
public boolean isUpdateCapable() {
return true;
}
public boolean isDeleteCapable() {
return true;
}
public void setDataStore(DataStore dataStore) throws LayerException {
super.setDataStore(dataStore);
}
@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, String> params = new HashMap<String, String>();
if (null != url) {
params.put("url", url);
}
if (null != dbtype) {
params.put("dbtype", dbtype);
}
if (null != parameters) {
for (Parameter parameter : parameters) {
params.put(parameter.getName(), parameter.getValue());
}
}
DataStore store = DataStoreFactory.create(params);
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);
}
}
}
public Object create(Object feature) throws LayerException {
FeatureSource<SimpleFeatureType, SimpleFeature> source = getFeatureSource();
if (source instanceof FeatureStore<?, ?>) {
SimpleFeatureStore store = (SimpleFeatureStore) source;
FeatureCollection<SimpleFeatureType, SimpleFeature> col = DataUtilities
.collection(new SimpleFeature[] { (SimpleFeature) feature });
if (transactionManager != null) {
store.setTransaction(transactionManager.getTransaction());
}
try {
store.addFeatures(col);
} 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 FeatureStore");
throw new LayerException(ExceptionCode.CREATE_OR_UPDATE_NOT_IMPLEMENTED, getFeatureSourceName(), source
.getClass().getName());
}
}
public void update(Object feature) throws LayerException {
FeatureSource<SimpleFeatureType, SimpleFeature> source = getFeatureSource();
if (source instanceof FeatureStore<?, ?>) {
SimpleFeatureStore store = (SimpleFeatureStore) source;
String featureId = getFeatureModel().getId(feature);
Filter filter = filterService.createFidFilter(new String[] {featureId});
if (transactionManager != null) {
store.setTransaction(transactionManager.getTransaction());
}
List<Name> names = new ArrayList<Name>();
Map<String, Attribute> attrMap = getFeatureModel().getAttributes(feature);
List<Object> values = new ArrayList<Object>();
for (String name : attrMap.keySet()) {
names.add(store.getSchema().getDescriptor(name).getName());
values.add(attrMap.get(name).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 FeatureStore");
throw new LayerException(ExceptionCode.CREATE_OR_UPDATE_NOT_IMPLEMENTED, getFeatureSourceName(), source
.getClass().getName());
}
}
public void delete(String featureId) throws LayerException {
FeatureSource<SimpleFeatureType, SimpleFeature> source = getFeatureSource();
if (source instanceof FeatureStore<?, ?>) {
SimpleFeatureStore store = (SimpleFeatureStore) source;
Filter filter = filterService.createFidFilter(new String[] {featureId});
if (transactionManager != null) {
store.setTransaction(transactionManager.getTransaction());
}
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 FeatureStore");
throw new LayerException(ExceptionCode.DELETE_NOT_IMPLEMENTED, getFeatureSourceName(), source.getClass()
.getName());
}
}
public Object saveOrUpdate(Object feature) throws LayerException {
if (exists(getFeatureModel().getId(feature))) {
update(feature);
} else {
feature = create(feature);
}
return feature;
}
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);
}
}
public Envelope getBounds() throws LayerException {
FeatureSource<SimpleFeatureType, SimpleFeature> source = getFeatureSource();
if (source instanceof FeatureStore<?, ?>) {
SimpleFeatureStore store = (SimpleFeatureStore) source;
if (transactionManager != null) {
store.setTransaction(transactionManager.getTransaction());
}
}
try {
FeatureCollection<SimpleFeatureType, SimpleFeature> fc = source.getFeatures();
FeatureIterator<SimpleFeature> it = fc.features();
if (transactionManager != null) {
transactionManager.addIterator(it);
}
return fc.getBounds();
} catch (Throwable t) {
throw new LayerException(t, ExceptionCode.UNEXPECTED_PROBLEM);
}
}
/**
* Retrieve the bounds of the specified features.
*
* @return the bounds of the specified features
*/
public Envelope getBounds(Filter filter) throws LayerException {
FeatureSource<SimpleFeatureType, SimpleFeature> source = getFeatureSource();
if (source instanceof FeatureStore<?, ?>) {
SimpleFeatureStore store = (SimpleFeatureStore) source;
if (transactionManager != null) {
store.setTransaction(transactionManager.getTransaction());
}
}
try {
FeatureCollection<SimpleFeatureType, SimpleFeature> fc = source.getFeatures(filter);
FeatureIterator<SimpleFeature> it = fc.features();
if (transactionManager != null) {
transactionManager.addIterator(it);
}
return fc.getBounds();
} catch (Throwable t) {
throw new LayerException(t, ExceptionCode.LAYER_MODEL_IO_EXCEPTION);
}
}
/**
* This implementation does not support the 'offset' and 'maxResultSize' parameters.
*/
public Iterator<?> getElements(Filter filter, int offset, int maxResultSize) throws LayerException {
FeatureSource<SimpleFeatureType, SimpleFeature> source = getFeatureSource();
if (source instanceof FeatureStore<?, ?>) {
SimpleFeatureStore store = (SimpleFeatureStore) source;
if (transactionManager != null) {
store.setTransaction(transactionManager.getTransaction());
}
}
try {
FeatureCollection<SimpleFeatureType, SimpleFeature> fc = source.getFeatures(filter);
FeatureIterator<SimpleFeature> it = fc.features();
if (transactionManager != null) {
transactionManager.addIterator(it);
}
return new JavaIterator(it);
} catch (Throwable t) {
throw new LayerException(t, ExceptionCode.UNEXPECTED_PROBLEM);
}
}
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 class JavaIterator implements Iterator {
private final FeatureIterator<SimpleFeature> delegate;
public JavaIterator(FeatureIterator<SimpleFeature> delegate) {
this.delegate = delegate;
}
public boolean hasNext() {
return delegate.hasNext();
}
public Object next() {
return delegate.next();
}
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.info("Retrying to initialize layer");
initFeatures();
} catch (Exception e) {
log.warn("Failed initializing layer: ", e.getMessage());
}
}
}
@Override
public SimpleFeatureSource getFeatureSource() throws LayerException {
if (!featureModelUsable) {
retryInitFeatures();
}
return super.getFeatureSource();
}
}