/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2009-2010, 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.wfs; import com.vividsolutions.jts.geom.Geometry; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import javax.xml.namespace.QName; import javax.xml.stream.XMLStreamException; import org.apache.sis.feature.FeatureTypeExt; import org.apache.sis.feature.ReprojectFeatureType; 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.feature.builder.PropertyTypeBuilder; import org.geotoolkit.data.AbstractFeatureStore; import org.geotoolkit.data.FeatureStoreFactory; import org.apache.sis.storage.DataStoreException; import org.geotoolkit.data.FeatureStoreUtilities; import org.geotoolkit.data.FeatureReader; import org.geotoolkit.data.FeatureCollection; import org.geotoolkit.data.FeatureWriter; import org.geotoolkit.data.memory.GenericEmptyFeatureIterator; import org.geotoolkit.data.memory.GenericWrapFeatureIterator; import org.geotoolkit.data.query.DefaultQueryCapabilities; import org.geotoolkit.data.query.Query; import org.geotoolkit.data.query.QueryCapabilities; import org.geotoolkit.factory.Hints; import org.geotoolkit.util.NamesExt; import org.geotoolkit.feature.xml.XmlFeatureReader; import org.geotoolkit.feature.xml.jaxb.JAXBFeatureTypeReader; import org.geotoolkit.feature.xml.jaxp.JAXPStreamFeatureReader; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.internal.storage.GenericNameMap; import org.geotoolkit.ows.xml.BoundingBox; import org.geotoolkit.parameter.Parameters; import org.apache.sis.referencing.CRS; import org.apache.sis.referencing.CommonCRS; import org.geotoolkit.wfs.xml.FeatureTypeList; import org.geotoolkit.wfs.xml.TransactionResponse; import org.geotoolkit.wfs.xml.WFSCapabilities; import org.geotoolkit.wfs.xml.WFSMarshallerPool; import org.opengis.util.GenericName; import org.geotoolkit.storage.DataStores; import org.opengis.feature.MismatchedFeatureException; import org.opengis.filter.Filter; import org.opengis.filter.identity.FeatureId; import org.opengis.geometry.Envelope; import org.opengis.util.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.referencing.crs.AbstractCRS; import org.apache.sis.referencing.cs.AxesConvention; import org.apache.sis.storage.IllegalNameException; import org.geotoolkit.data.FeatureStoreRuntimeException; import org.geotoolkit.data.internal.GenericNameIndex; import org.geotoolkit.data.memory.GenericDecoratedFeatureIterator; import org.opengis.feature.Feature; import org.opengis.feature.FeatureType; /** * WFS Datastore, This implementation is read only. * * @author Johann Sorel (Geomatys) * @module */ public class WFSFeatureStore extends AbstractFeatureStore{ private static final AtomicLong NS_INC = new AtomicLong(); private final QueryCapabilities queryCapabilities = new DefaultQueryCapabilities(false); private final WebFeatureClient server; private final List<GenericName> typeNames = new ArrayList<>(); private final GenericNameIndex<FeatureType> types = new GenericNameIndex<>(); private final GenericNameIndex<Envelope> bounds = new GenericNameIndex<>(); private final Map<String,String> prefixes = new HashMap<>(); public WFSFeatureStore(WebFeatureClient server) throws WebFeatureException { super(server.getConfiguration()); this.server = server; try { checkTypeExist(); } catch (IllegalNameException ex) { getLogger().log(Level.WARNING, ex.getMessage(), ex); throw new FeatureStoreRuntimeException(ex); } } private void checkTypeExist() throws WebFeatureException, IllegalNameException { final WFSCapabilities capabilities = server.getCapabilities(); final FeatureTypeList lst = capabilities.getFeatureTypeList(); for(final org.geotoolkit.wfs.xml.FeatureType ftt : lst.getFeatureType()){ //extract the name ------------------------------------------------- QName typeName = ftt.getName(); String prefix = typeName.getPrefix(); final String uri = typeName.getNamespaceURI(); final String localpart = typeName.getLocalPart(); if(prefix == null || prefix.isEmpty()){ prefix = "geotk" + NS_INC.incrementAndGet(); } GenericName name = NamesExt.create(uri, localpart); typeName = new QName(uri, localpart, prefix); //extract the feature type ----------------------------------------- CoordinateReferenceSystem crs; FeatureType sft; try { String defaultCRS = ftt.getDefaultCRS(); if(defaultCRS.contains("EPSG")){ final int last = defaultCRS.lastIndexOf(':'); defaultCRS = "EPSG:"+defaultCRS.substring(last+1); } crs = CRS.forCode(defaultCRS); if (getLongitudeFirst()) { crs = AbstractCRS.castOrCopy(crs).forConvention(AxesConvention.RIGHT_HANDED); } sft = requestType(typeName); } catch (IOException ex) { getLogger().log(Level.WARNING, null, ex); continue; } catch (FactoryException ex) { getLogger().log(Level.WARNING, null, ex); continue; } final FeatureTypeBuilder sftb = new FeatureTypeBuilder(sft); AttributeTypeBuilder geomDesc = null; for (PropertyTypeBuilder pt : sftb.properties()) { if (pt instanceof AttributeTypeBuilder && Geometry.class.isAssignableFrom(((AttributeTypeBuilder)pt).getValueClass())){ ((AttributeTypeBuilder)pt).setCRS(crs); if(geomDesc==null){ geomDesc = (AttributeTypeBuilder) pt; geomDesc.addRole(AttributeRole.DEFAULT_GEOMETRY); } } } sft = sftb.build(); name = sft.getName(); types.add(name, sft); prefixes.put(NamesExt.getNamespace(name), prefix); typeNames.add(name); if(geomDesc != null){ final CoordinateReferenceSystem val = geomDesc.getCRS(); if(val == null){ throw new IllegalArgumentException("CRS should not be null"); } //extract the bounds ----------------------------------------------- final BoundingBox bbox = ftt.getBoundingBox().get(0); try { final String crsVal = bbox.getCrs(); crs = crsVal != null ? CRS.forCode(crsVal) : CommonCRS.WGS84.normalizedGeographic(); final GeneralEnvelope env = new GeneralEnvelope(crs); final Integer dims = bbox.getDimensions(); final List<Double> upper = bbox.getUpperCorner(); final List<Double> lower = bbox.getLowerCorner(); //@TODO bbox should be null if there is no bbox in response. if(dims == null) {continue;} for(int i=0,n=dims.intValue();i<n;i++){ env.setRange(i, lower.get(i), upper.get(i)); } bounds.add(name, env); } catch (FactoryException ex) { getLogger().log(Level.WARNING, null, ex); } } } } public boolean getUsePost(){ return Parameters.value(WFSFeatureStoreFactory.POST_REQUEST, parameters); } public boolean getLongitudeFirst(){ return Parameters.getOrCreate(WFSFeatureStoreFactory.LONGITUDE_FIRST, parameters).booleanValue(); } @Override public FeatureStoreFactory getFactory() { return (FeatureStoreFactory) DataStores.getFactoryById(WFSFeatureStoreFactory.NAME); } @Override public boolean isWritable(final String typeName) throws DataStoreException { this.typeCheck(typeName); return true; } /** * {@inheritDoc } */ @Override public Set<GenericName> getNames() throws DataStoreException { return types.getNames(); } /** * {@inheritDoc } */ @Override public FeatureType getFeatureType(final String typeName) throws DataStoreException { final FeatureType ft = types.get(typeName); if(ft == null){ throw new DataStoreException("Type : "+ typeName + " doesn't exist in this datastore."); } return ft; } /** * {@inheritDoc } */ @Override public Envelope getEnvelope(final Query query) throws DataStoreException { final String typeName = query.getTypeName(); final FeatureType type = getFeatureType(typeName); if( query.getCoordinateSystemReproject() == null && query.getFilter() == Filter.INCLUDE && (query.getMaxFeatures() == null || query.getMaxFeatures() == Integer.MAX_VALUE) && query.getStartIndex() == 0){ Envelope env = bounds.get(type.getName().toString()); if(env != null) {return env;} } return super.getEnvelope(query); } /** * {@inheritDoc } */ @Override public QueryCapabilities getQueryCapabilities() { return queryCapabilities; } //////////////////////////////////////////////////////////////////////////// // schema manipulation ///////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /** * {@inheritDoc } */ @Override public void createFeatureType(final FeatureType featureType) throws DataStoreException { throw new DataStoreException("Schema creation not supported."); } /** * {@inheritDoc } */ @Override public void updateFeatureType(final FeatureType featureType) throws DataStoreException { throw new DataStoreException("Schema update not supported."); } /** * {@inheritDoc } */ @Override public void deleteFeatureType(final String typeName) throws DataStoreException { throw new DataStoreException("Schema deletion not supported."); } //////////////////////////////////////////////////////////////////////////// // read & write //////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /** * {@inheritDoc } */ @Override public FeatureReader getFeatureReader(final Query query) throws DataStoreException { final String name = query.getTypeName(); //will raise an error if typename in unknowned final FeatureType sft = getFeatureType(name); final QName q = new QName(NamesExt.getNamespace(sft.getName()), sft.getName().tip().toString(), prefixes.get(NamesExt.getNamespace(sft.getName()))); final FeatureCollection collection; try { collection = requestFeature(q, query); } catch (IOException ex) { throw new DataStoreException(ex); } FeatureReader reader; if(collection == null){ reader = GenericEmptyFeatureIterator.createReader(sft); }else{ reader = GenericWrapFeatureIterator.wrapToReader(collection.iterator(), sft); } //we handle reprojection ourself, too complex or never done properly for a large //majority of wfs server tested. if(query.getCoordinateSystemReproject() != null){ try { reader = GenericDecoratedFeatureIterator.wrap(reader, new ReprojectFeatureType(reader.getFeatureType(), query.getCoordinateSystemReproject()), null); } catch (MismatchedFeatureException ex) { getLogger().log(Level.WARNING, ex.getMessage(), ex); } } return reader; } /** * Writer that fall back on add,remove, update methods. */ @Override public FeatureWriter getFeatureWriter(Query query) throws DataStoreException { return handleWriter(query); } /** * {@inheritDoc } */ @Override public List<FeatureId> addFeatures(final String groupName, final Collection<? extends Feature> newFeatures, final Hints hints) throws DataStoreException { final FeatureType featureType = getFeatureType(groupName); final TransactionRequest request = server.createTransaction(); final Insert insert = server.createInsertElement(); insert.setInputFormat("text/xml; subtype=gml/3.1.1"); final FeatureCollection col; if(newFeatures instanceof FeatureCollection){ col = (FeatureCollection) newFeatures; }else{ col = FeatureStoreUtilities.collection("", null); col.addAll(newFeatures); } insert.setFeatures(col); request.elements().add(insert); InputStream response = null; try { response = request.getResponseStream(); Unmarshaller unmarshal = WFSMarshallerPool.getInstance().acquireUnmarshaller(); Object obj = unmarshal.unmarshal(response); WFSMarshallerPool.getInstance().recycle(unmarshal); if(obj instanceof JAXBElement){ obj = ((JAXBElement)obj).getValue(); } if(obj instanceof TransactionResponse){ final TransactionResponse tr = (TransactionResponse) obj; fireFeaturesAdded(featureType.getName(), null); // TODO list the feature added return tr.getInsertedFID(); }else{ throw new DataStoreException("Unexpected response : "+ obj.getClass()); } } catch (IOException ex) { throw new DataStoreException(ex); } catch (JAXBException ex) { throw new DataStoreException(ex); } finally { if(response != null){ try { response.close(); } catch (IOException ex) { getLogger().log(Level.SEVERE, null, ex); } } } } /** * {@inheritDoc } */ @Override public void updateFeatures(final String groupName, final Filter filter, final Map<String, ? extends Object> values) throws DataStoreException { final FeatureType featureType = getFeatureType(groupName); final TransactionRequest request = server.createTransaction(); final Update update = server.createUpdateElement(); update.setInputFormat("text/xml; subtype=gml/3.1.1"); update.setFilter(filter); update.setTypeName(featureType.getName()); for(Map.Entry<String,? extends Object> entry : values.entrySet()){ update.updates().put(featureType.getProperty(entry.getKey()), entry.getValue()); } request.elements().add(update); try { final InputStream response = request.getResponseStream(); response.close(); fireFeaturesUpdated(featureType.getName(), null);// TODO list the feature updated } catch (IOException ex) { throw new DataStoreException(ex); } } /** * {@inheritDoc } */ @Override public void removeFeatures(final String groupName, final Filter filter) throws DataStoreException { final FeatureType featureType = getFeatureType(groupName); final TransactionRequest request = server.createTransaction(); final Delete delete = server.createDeleteElement(); delete.setTypeName(featureType.getName()); delete.setFilter(filter); request.elements().add(delete); try { final InputStream response = request.getResponseStream(); response.close(); fireFeaturesDeleted(featureType.getName(), null);// TODO list the feature deleted } catch (IOException ex) { throw new DataStoreException(ex); } } //////////////////////////////////////////////////////////////////////////// // read & write //////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// private FeatureType requestType(final QName typeName) throws IOException{ final DescribeFeatureTypeRequest request = server.createDescribeFeatureType(); request.setTypeNames(Collections.singletonList(typeName)); try { final JAXBFeatureTypeReader reader = new JAXBFeatureTypeReader(); final InputStream stream; if (getUsePost()) { getLogger().log(Level.INFO, "[WFS Client] request type by POST."); stream = request.getResponseStream(); } else { getLogger().log(Level.INFO, "[WFS Client] request type : {0}", request.getURL()); stream = request.getURL().openStream(); } final List<FeatureType> featureTypes = reader.read(stream); return featureTypes.get(0); } catch (MalformedURLException ex) { throw new IOException(ex); } catch (JAXBException ex) { throw new IOException(ex); } } private FeatureCollection requestFeature(final QName typeName, final Query query) throws IOException, IllegalNameException { final GenericName name = NamesExt.create(typeName); FeatureType type = types.get(name.toString()); type = FeatureTypeExt.createSubType(type, query.getPropertyNames()); final GetFeatureRequest request = server.createGetFeature(); request.setTypeName(typeName); if(query != null){ final Filter filter = query.getFilter(); if(filter == null){ request.setFilter(Filter.INCLUDE); }else{ request.setFilter(filter); } final Integer max = query.getMaxFeatures(); if(max != null){ request.setMaxFeatures(max); } final String[] propertyNames = query.getPropertyNames(); GenericName[] names = null; if (propertyNames!=null) { names = new GenericName[propertyNames.length]; for(int i=0;i<propertyNames.length;i++) { names[i] = type.getProperty(propertyNames[i]).getName(); } } request.setPropertyNames(names); } XmlFeatureReader reader = null; try { reader = new JAXPStreamFeatureReader(type); reader.getProperties().put(JAXPStreamFeatureReader.SKIP_UNEXPECTED_PROPERTY_TAGS, true); final InputStream stream; if (getUsePost()) { getLogger().log(Level.INFO, "[WFS Client] request feature by POST."); stream = request.getResponseStream(); } else { final URL url = request.getURL(); getLogger().log(Level.INFO, "[WFS Client] request feature : {0}", url); stream = url.openStream(); } final Object result = reader.read(stream); if(result instanceof Feature){ final Feature sf = (Feature) result; final FeatureCollection col = FeatureStoreUtilities.collection("id", type); col.add(sf); return col; }else if(result instanceof FeatureCollection){ final FeatureCollection col = (FeatureCollection) result; return col; }else{ final FeatureCollection col = FeatureStoreUtilities.collection("", type); return col; } }catch (XMLStreamException ex) { throw new IOException(ex); }finally{ if(reader != null){ reader.dispose(); } } } @Override public void refreshMetaModel() throws IllegalNameException { types.clear(); prefixes.clear(); typeNames.clear(); bounds.clear(); checkTypeExist(); } }