Fix bbox query for box overlap
[gpl/argeo-suite.git] / org.argeo.app.geo / src / org / argeo / app / geo / http / WfsHttpHandler.java
index 92f41ac1ab9088c52f8f755508b7efb614389eff..c12a77b9da24cc0226629d270188b5ee816d0194 100644 (file)
@@ -6,50 +6,62 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.UncheckedIOException;
 import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
 
 import javax.xml.namespace.QName;
 
 import org.argeo.api.acr.Content;
 import org.argeo.api.acr.ContentSession;
-import org.argeo.api.acr.DName;
 import org.argeo.api.acr.NamespaceUtils;
-import org.argeo.api.acr.QNamed;
 import org.argeo.api.acr.ldap.LdapAttr;
+import org.argeo.api.acr.search.AndFilter;
 import org.argeo.api.acr.spi.ProvidedRepository;
 import org.argeo.api.cms.CmsLog;
 import org.argeo.app.api.EntityName;
 import org.argeo.app.api.EntityType;
 import org.argeo.app.api.WGS84PosName;
+import org.argeo.app.api.geo.FeatureAdapter;
+import org.argeo.app.api.geo.WfsKvp;
 import org.argeo.app.geo.CqlUtils;
-import org.argeo.app.geo.GeoJSon;
+import org.argeo.app.geo.GeoJson;
+import org.argeo.app.geo.GeoUtils;
 import org.argeo.app.geo.GpxUtils;
 import org.argeo.app.geo.JTS;
+import org.argeo.app.geo.acr.GeoEntityUtils;
+import org.argeo.cms.acr.json.AcrJsonUtils;
+import org.argeo.cms.auth.RemoteAuthUtils;
 import org.argeo.cms.http.HttpHeader;
+import org.argeo.cms.http.RemoteAuthHttpExchange;
 import org.argeo.cms.http.server.HttpServerUtils;
-import org.geotools.data.DataUtilities;
-import org.geotools.data.geojson.GeoJSONWriter;
+import org.argeo.cms.util.LangUtils;
+import org.geotools.api.feature.GeometryAttribute;
+import org.geotools.api.feature.simple.SimpleFeature;
+import org.geotools.api.feature.simple.SimpleFeatureType;
+import org.geotools.api.feature.type.AttributeDescriptor;
+import org.geotools.api.feature.type.Name;
+import org.geotools.api.referencing.FactoryException;
+import org.geotools.api.referencing.crs.CoordinateReferenceSystem;
+import org.geotools.api.referencing.operation.MathTransform;
+import org.geotools.api.referencing.operation.TransformException;
 import org.geotools.feature.DefaultFeatureCollection;
 import org.geotools.feature.NameImpl;
-import org.geotools.feature.SchemaException;
 import org.geotools.feature.simple.SimpleFeatureBuilder;
-import org.geotools.geometry.jts.JTSFactoryFinder;
+import org.geotools.referencing.CRS;
 import org.geotools.referencing.crs.DefaultGeographicCRS;
 import org.geotools.wfs.GML;
 import org.geotools.wfs.GML.Version;
 import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Envelope;
 import org.locationtech.jts.geom.Geometry;
-import org.locationtech.jts.geom.GeometryFactory;
-import org.locationtech.jts.geom.Point;
 import org.locationtech.jts.geom.Polygon;
-import org.opengis.feature.GeometryAttribute;
-import org.opengis.feature.simple.SimpleFeature;
-import org.opengis.feature.simple.SimpleFeatureType;
-import org.opengis.feature.type.AttributeDescriptor;
-import org.opengis.feature.type.Name;
 
 import com.sun.net.httpserver.HttpExchange;
 import com.sun.net.httpserver.HttpHandler;
@@ -62,74 +74,220 @@ public class WfsHttpHandler implements HttpHandler {
        private final static CmsLog log = CmsLog.getLog(WfsHttpHandler.class);
        private ProvidedRepository contentRepository;
 
-       // HTTP parameters
-       final static String OUTPUT_FORMAT = "outputFormat";
-       final static String TYPE_NAMES = "typeNames";
-       final static String CQL_FILTER = "cql_filter";
+       private final Map<QName, FeatureAdapter> featureAdapters = new HashMap<>();
 
        @Override
        public void handle(HttpExchange exchange) throws IOException {
-               String path = HttpServerUtils.subPath(exchange);
                ContentSession session = HttpServerUtils.getContentSession(contentRepository, exchange);
-               // Content content = session.get(path);
+
+               String path = HttpServerUtils.subPath(exchange);
+
+               // content path
+               final String pathToUse = path;
+               String fileName = null;
+               boolean zipped = false;
+//             int lastSlash = path.lastIndexOf('/');
+//             if (lastSlash > 0) {
+//                     fileName = path.substring(lastSlash + 1);
+//             }
+//             if (fileName != null) {
+//                     pathToUse = path.substring(0, lastSlash);
+//                     if (path.endsWith(".zip")) {
+//                             zipped = true;
+//                     }
+//             } else {
+//                     pathToUse = path;
+//             }
 
                Map<String, List<String>> parameters = HttpServerUtils.parseParameters(exchange);
-               String cql = parameters.containsKey(CQL_FILTER) ? parameters.get(CQL_FILTER).get(0) : null;
-               String typeNamesStr = parameters.containsKey(TYPE_NAMES) ? parameters.get(TYPE_NAMES).get(0) : null;
-               String outputFormat = parameters.containsKey(OUTPUT_FORMAT) ? parameters.get(OUTPUT_FORMAT).get(0) : null;
+
+               // PARAMETERS
+               String cql = getKvpParameter(parameters, WfsKvp.CQL_FILTER);
+               String typeNamesStr = getKvpParameter(parameters, WfsKvp.TYPE_NAMES);
+               String outputFormat = getKvpParameter(parameters, WfsKvp.OUTPUT_FORMAT);
                if (outputFormat == null) {
                        outputFormat = "application/json";
                }
 
-               switch (outputFormat) {
-               case "application/json" -> {
-                       exchange.getResponseHeaders().set(HttpHeader.CONTENT_TYPE.getHeaderName(), "application/json");
+               // TODO deal with multiple
+               String formatOption = getKvpParameter(parameters, WfsKvp.FORMAT_OPTIONS);
+               if (formatOption != null) {
+                       if (formatOption.startsWith(WfsKvp.FILENAME_))
+                               fileName = formatOption.substring(WfsKvp.FILENAME_.length());
                }
-               case "GML3" -> {
-//                     exchange.getResponseHeaders().set(HttpHeader.CONTENT_TYPE.getHeaderName(), "application/gml+xml");
-                       exchange.getResponseHeaders().set(HttpHeader.CONTENT_TYPE.getHeaderName(), "application/xml");
+               if (fileName != null && fileName.endsWith(".zip"))
+                       zipped = true;
+
+               // bbox
+               String bboxStr = getKvpParameter(parameters, WfsKvp.BBOX);
+               if (log.isTraceEnabled())
+                       log.trace(bboxStr);
+               final Envelope bbox;
+               if (bboxStr != null) {
+                       String srs;
+                       String[] arr = bboxStr.split(",");
+                       // TODO check SRS and convert to WGS84
+                       double minLat = Double.parseDouble(arr[0]);
+                       double minLon = Double.parseDouble(arr[1]);
+                       double maxLat = Double.parseDouble(arr[2]);
+                       double maxLon = Double.parseDouble(arr[3]);
+                       if (arr.length == 5) {
+                               srs = arr[4];
+                       } else {
+                               srs = null;
+                       }
+
+                       if (srs != null && !srs.equals(GeoUtils.EPSG_4326)) {
+                               try {
+                                       // TODO optimise
+                                       CoordinateReferenceSystem sourceCRS = CRS.decode(srs);
+                                       CoordinateReferenceSystem targetCRS = CRS.decode(GeoUtils.EPSG_4326);
+                                       MathTransform transform = CRS.findMathTransform(sourceCRS, targetCRS, true);
+                                       bbox = org.geotools.geometry.jts.JTS.transform(
+                                                       new Envelope(new Coordinate(minLat, minLon), new Coordinate(maxLat, maxLon)), transform);
+                               } catch (FactoryException | TransformException e) {
+                                       throw new IllegalArgumentException("Cannot convert bounding box", e);
+                                       // bbox = null;
+                               }
+                       } else {
+                               bbox = new Envelope(new Coordinate(minLat, minLon), new Coordinate(maxLat, maxLon));
+                       }
+               } else {
+                       bbox = null;
                }
 
-               default -> throw new IllegalArgumentException("Unexpected value: " + outputFormat);
+               // response headers
+               exchange.getResponseHeaders().set(HttpHeader.DATE.getHeaderName(), Long.toString(System.currentTimeMillis()));
+
+               if (fileName != null) {
+                       exchange.getResponseHeaders().set(HttpHeader.CONTENT_DISPOSITION.getHeaderName(),
+                                       HttpHeader.ATTACHMENT + ";" + HttpHeader.FILENAME + "=\"" + fileName + "\"");
+
+               }
+
+               // content type
+               if (zipped) {
+                       exchange.getResponseHeaders().set(HttpHeader.CONTENT_TYPE.getHeaderName(), "application/zip");
+
+               } else {
+                       switch (outputFormat) {
+                       case "application/json" -> {
+                               exchange.getResponseHeaders().set(HttpHeader.CONTENT_TYPE.getHeaderName(), "application/json");
+                       }
+                       case "GML3" -> {
+//                     exchange.getResponseHeaders().set(HttpHeader.CONTENT_TYPE.getHeaderName(), "application/gml+xml");
+                               exchange.getResponseHeaders().set(HttpHeader.CONTENT_TYPE.getHeaderName(), "application/xml");
+                       }
+
+                       default -> throw new IllegalArgumentException("Unexpected value: " + outputFormat);
+                       }
                }
 
-               QName[] typeNames;
+               List<QName> typeNames = new ArrayList<>();
                if (typeNamesStr != null) {
                        String[] arr = typeNamesStr.split(",");
-                       typeNames = new QName[arr.length];
                        for (int i = 0; i < arr.length; i++) {
-                               typeNames[i] = NamespaceUtils.parsePrefixedName(arr[i]);
+                               typeNames.add(NamespaceUtils.parsePrefixedName(arr[i]));
                        }
                } else {
-                       typeNames = new QName[] { EntityType.local.qName() };
+                       typeNames.add(EntityType.local.qName());
                }
 
+               if (typeNames.size() > 1)
+                       throw new UnsupportedOperationException("Only one type name is currently supported");
+
+               // QUERY
                Stream<Content> res = session.search((search) -> {
                        if (cql != null) {
-                               CqlUtils.filter(search.from(path), cql);
+                               CqlUtils.filter(search.from(pathToUse), cql);
                        } else {
-                               search.from(path).where((and) -> {
+                               search.from(pathToUse);
+                       }
+                       for (QName typeName : typeNames) {
+                               FeatureAdapter featureAdapter = featureAdapters.get(typeName);
+                               if (featureAdapter == null)
+                                       throw new IllegalStateException("No feature adapter found for " + typeName);
+                               // f.isContentClass(typeName);
+                               RemoteAuthUtils.doAs(() -> {
+                                       featureAdapter.addConstraintsForFeature((AndFilter) search.getWhere(), typeName);
+                                       return null;
+                               }, new RemoteAuthHttpExchange(exchange));
+                       }
+
+                       if (bbox != null) {
+                               search.getWhere().any((or) -> {
+                                       // box overlap, see https://stackoverflow.com/questions/20925818/algorithm-to-check-if-two-boxes-overlap
+                                       // isOverlapping = (x1min < x2max AND x2min < x1max AND y1min < y2max AND y2min < y1max)
+                                       // x1 = entity, x2 = bbox
+                                       or.all((and) -> {
+                                               and.lte(EntityName.minLat, bbox.getMaxX());
+                                               and.gte(EntityName.maxLat, bbox.getMinX());
+                                               and.lte(EntityName.minLon, bbox.getMaxY());
+                                               and.gte(EntityName.maxLon, bbox.getMinY());
+                                       });
+                                       or.all((and) -> {
+                                               and.gte(WGS84PosName.lat, bbox.getMinX());
+                                               and.gte(WGS84PosName.lon, bbox.getMinY());
+                                               and.lte(WGS84PosName.lat, bbox.getMaxX());
+                                               and.lte(WGS84PosName.lon, bbox.getMaxY());
+                                       });
                                });
                        }
-                       search.getWhere().any((f) -> {
-                               for (QName typeName : typeNames)
-                                       f.isContentClass(typeName);
-                       });
                });
 
                exchange.sendResponseHeaders(200, 0);
 
                final int BUFFER_SIZE = 100 * 1024;
-               try (BufferedOutputStream out = new BufferedOutputStream(exchange.getResponseBody(), BUFFER_SIZE)) {
+               try (OutputStream out = zipped ? new ZipOutputStream(exchange.getResponseBody())
+                               : new BufferedOutputStream(exchange.getResponseBody(), BUFFER_SIZE)) {
+                       if (out instanceof ZipOutputStream zipOut) {
+                               String unzippedFileName = fileName.substring(0, fileName.length() - ".zip".length());
+                               zipOut.putNextEntry(new ZipEntry(unzippedFileName));
+                       }
+
                        if ("GML3".equals(outputFormat)) {
                                encodeCollectionAsGML(res, out);
                        } else if ("application/json".equals(outputFormat)) {
-                               encodeCollectionAsGeoJSon(res, out);
+                               encodeCollectionAsGeoJSon(res, out, typeNames);
+                       }
+               }
+       }
+
+       /**
+        * Retrieve KVP (keyword-value pairs) parameters, which are lower case, as per
+        * specifications.
+        * 
+        * @see https://docs.ogc.org/is/09-025r2/09-025r2.html#19
+        */
+       protected String getKvpParameter(Map<String, List<String>> parameters, WfsKvp key) {
+               Objects.requireNonNull(key, "KVP key cannot be null");
+               // let's first try the default (CAML case) which should be more efficient
+               List<String> values = parameters.get(key.getKey());
+               if (values == null) {
+                       // then let's do an ignore case comparison of the key
+                       keys: for (String k : parameters.keySet()) {
+                               if (key.getKey().equalsIgnoreCase(k)) {
+                                       values = parameters.get(k);
+                                       break keys;
+                               }
                        }
                }
+               if (values == null) // nothing was found
+                       return null;
+               if (values.size() != 1) {
+                       // although not completely clear from the standard, we assume keys must be
+                       // unique
+                       // since lists are defined here
+                       // https://docs.ogc.org/is/09-026r2/09-026r2.html#10
+                       throw new IllegalArgumentException("Key " + key + " as multiple values");
+               }
+               String value = values.get(0);
+               assert value != null;
+               return value;
        }
 
-       protected void encodeCollectionAsGeoJSon(Stream<Content> features, OutputStream out) throws IOException {
+       protected void encodeCollectionAsGeoJSon(Stream<Content> features, OutputStream out, List<QName> typeNames)
+                       throws IOException {
                long begin = System.currentTimeMillis();
                AtomicLong count = new AtomicLong(0);
                JsonGenerator generator = Json.createGenerator(out);
@@ -137,20 +295,62 @@ public class WfsHttpHandler implements HttpHandler {
                generator.write("type", "FeatureCollection");
                generator.writeStartArray("features");
                features.forEach((c) -> {
-                       Geometry defaultGeometry = getDefaultGeometry(c);
-                       if (defaultGeometry == null)
-                               return;
-                       generator.writeStartObject();
-                       generator.write("type", "Feature");
-                       String featureId = getFeatureId(c);
-                       if (featureId != null)
-                               generator.write("id", featureId);
-                       GeoJSon.writeBBox(generator, defaultGeometry);
-                       GeoJSon.writeGeometry(generator, defaultGeometry);
-
-                       generator.writeStartObject("properties");
-                       writeTimeProperties(generator, c);
-                       writeProperties(generator, c);
+                       // TODO deal with multiple type names
+                       FeatureAdapter featureAdapter = null;
+                       QName typeName = null;
+                       if (!typeNames.isEmpty()) {
+                               typeName = typeNames.get(0);
+                               featureAdapter = featureAdapters.get(typeName);
+                       }
+
+                       boolean geometryWritten = false;
+//                     if (typeName.getLocalPart().equals("fieldSimpleFeature")) {
+//                             Content area = c.getContent("place.geom.json").orElse(null);
+//                             if (area != null) {
+//                                     generator.writeStartObject();
+//                                     generator.write("type", "Feature");
+//                                     String featureId = getFeatureId(c);
+//                                     if (featureId != null)
+//                                             generator.write("id", featureId);
+//
+//                                     generator.flush();
+//                                     try (InputStream in = area.open(InputStream.class)) {
+//                                             out.write(",\"geometry\":".getBytes());
+//                                             StreamUtils.copy(in, out);                                              
+//                                             //out.flush();
+//                                     } catch (Exception e) {
+//                                             log.error(c.getPath() + " : " + e.getMessage());
+//                                     } finally {
+//                                     }
+//                                     geometryWritten = true;
+//                             }else {
+//                                     return;
+//                             }
+//                     }
+
+                       if (!geometryWritten) {
+
+                               Geometry defaultGeometry = featureAdapter != null ? featureAdapter.getDefaultGeometry(c, typeName)
+                                               : getDefaultGeometry(c);
+                               if (defaultGeometry == null)
+                                       return;
+                               generator.writeStartObject();
+                               generator.write("type", "Feature");
+                               String featureId = getFeatureId(c);
+                               if (featureId != null)
+                                       generator.write("id", featureId);
+
+                               GeoJson.writeBBox(generator, defaultGeometry);
+                               generator.writeStartObject(GeoJson.GEOMETRY);
+                               GeoJson.writeGeometry(generator, defaultGeometry);
+                               generator.writeEnd();// geometry object
+                       }
+                       generator.writeStartObject(GeoJson.PROPERTIES);
+                       AcrJsonUtils.writeTimeProperties(generator, c);
+                       if (featureAdapter != null)
+                               featureAdapter.writeProperties(generator, c, typeName);
+                       else
+                               writeProperties(generator, c);
                        generator.writeEnd();// properties object
 
                        generator.writeEnd();// feature object
@@ -170,12 +370,7 @@ public class WfsHttpHandler implements HttpHandler {
 
        protected Geometry getDefaultGeometry(Content content) {
                if (content.hasContentClass(EntityType.geopoint)) {
-                       double latitude = content.get(WGS84PosName.lat, Double.class).get();
-                       double longitude = content.get(WGS84PosName.lon, Double.class).get();
-
-                       Coordinate coordinate = new Coordinate(longitude, latitude);
-                       Point the_geom = JTS.GEOMETRY_FACTORY.createPoint(coordinate);
-                       return the_geom;
+                       return GeoEntityUtils.toPoint(content);
                }
                return null;
        }
@@ -185,24 +380,7 @@ public class WfsHttpHandler implements HttpHandler {
                return uuid;
        }
 
-       private final QName JCR_CREATED = NamespaceUtils.parsePrefixedName("jcr:created");
-
-       private final QName JCR_LAST_MODIFIED = NamespaceUtils.parsePrefixedName("jcr:lastModified");
-
-       protected void writeTimeProperties(JsonGenerator g, Content content) {
-               String creationDate = content.attr(DName.creationdate);
-               if (creationDate == null)
-                       creationDate = content.attr(JCR_CREATED);
-               if (creationDate != null)
-                       g.write(DName.creationdate.get(), creationDate);
-               String lastModified = content.attr(DName.getlastmodified);
-               if (lastModified == null)
-                       lastModified = content.attr(JCR_LAST_MODIFIED);
-               if (lastModified != null)
-                       g.write(DName.getlastmodified.get(), lastModified);
-       }
-
-       protected void writeProperties(JsonGenerator generator, Content content) {
+       public void writeProperties(JsonGenerator generator, Content content) {
                String path = content.getPath();
                generator.write("path", path);
                if (content.hasContentClass(EntityType.local)) {
@@ -217,105 +395,6 @@ public class WfsHttpHandler implements HttpHandler {
 
        }
 
-       protected void writeAttr(JsonGenerator g, Content content, String attr) {
-               writeAttr(g, content, NamespaceUtils.parsePrefixedName(attr));
-       }
-
-       protected void writeAttr(JsonGenerator g, Content content, QNamed attr) {
-               writeAttr(g, content, attr.qName());
-       }
-
-       protected void writeAttr(JsonGenerator g, Content content, QName attr) {
-               // String value = content.attr(attr);
-               Object value = content.get(attr);
-               if (value != null) {
-                       // TODO specify NamespaceContext
-                       String key = NamespaceUtils.toPrefixedName(attr);
-                       if (value instanceof Double v)
-                               g.write(key, v);
-                       else if (value instanceof Long v)
-                               g.write(key, v);
-                       else if (value instanceof Integer v)
-                               g.write(key, v);
-                       else if (value instanceof Boolean v)
-                               g.write(key, v);
-                       else
-                               g.write(key, value.toString());
-               }
-       }
-
-       protected void encodeCollectionAsGeoJSonOld(Stream<Content> features, OutputStream out) throws IOException {
-
-               // BODY PROCESSING
-               try (GeoJSONWriter geoJSONWriter = new GeoJSONWriter(out)) {
-                       geoJSONWriter.setPrettyPrinting(true);
-                       geoJSONWriter.setEncodeFeatureBounds(true);
-
-                       boolean gpx = true;
-                       SimpleFeatureType TYPE;
-                       try {
-                               if (gpx)
-                                       TYPE = DataUtilities.createType("Content",
-                                                       "the_geom:Polygon:srid=4326,path:String,type:String,name:String");
-                               else
-                                       TYPE = DataUtilities.createType("Content",
-                                                       "the_geom:Point:srid=4326,path:String,type:String,name:String");
-                       } catch (SchemaException e) {
-                               throw new RuntimeException(e);
-                       }
-                       SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(TYPE);
-                       GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory();
-
-                       features.forEach((c) -> {
-                               Geometry the_geom;
-                               if (gpx) {// experimental
-                                       Content area = c.getContent("gpx/area.gpx").orElse(null);
-                                       if (area == null)
-                                               return;
-                                       try (InputStream in = area.open(InputStream.class)) {
-                                               SimpleFeature feature = GpxUtils.parseGpxToPolygon(in);
-                                               the_geom = (Geometry) feature.getDefaultGeometry();
-                                       } catch (IOException e) {
-                                               throw new UncheckedIOException("Cannot parse " + c, e);
-                                       }
-                               } else {
-                                       if (!c.hasContentClass(EntityType.geopoint))
-                                               return;
-
-                                       double latitude = c.get(WGS84PosName.lat, Double.class).get();
-                                       double longitude = c.get(WGS84PosName.lon, Double.class).get();
-
-                                       Coordinate coordinate = new Coordinate(longitude, latitude);
-                                       the_geom = geometryFactory.createPoint(coordinate);
-
-                               }
-
-                               featureBuilder.add(the_geom);
-                               String pth = c.getPath();
-                               featureBuilder.add(pth);
-                               if (c.hasContentClass(EntityType.local)) {
-                                       String type = c.attr(EntityName.type);
-                                       featureBuilder.add(type);
-                               } else {
-                                       List<QName> contentClasses = c.getContentClasses();
-                                       if (!contentClasses.isEmpty()) {
-                                               featureBuilder.add(NamespaceUtils.toPrefixedName(contentClasses.get(0)));
-                                       }
-                               }
-                               featureBuilder.add(NamespaceUtils.toPrefixedName(c.getName()));
-
-                               String uuid = c.attr(LdapAttr.entryUUID);
-
-                               SimpleFeature feature = featureBuilder.buildFeature(uuid);
-                               try {
-                                       geoJSONWriter.write(feature);
-                               } catch (IOException e) {
-                                       throw new UncheckedIOException(e);
-                               }
-                       });
-               }
-       }
-
        protected void encodeCollectionAsGML(Stream<Content> features, OutputStream out) throws IOException {
                String entityType = "entity";
                URL schemaLocation = getClass().getResource("/org/argeo/app/api/entity.xsd");
@@ -393,6 +472,36 @@ public class WfsHttpHandler implements HttpHandler {
 
        }
 
+       /*
+        * DEPENDENCY INJECTION
+        */
+
+       public void addFeatureAdapter(FeatureAdapter featureAdapter, Map<String, Object> properties) {
+               List<String> typeNames = LangUtils.toStringList(properties.get(WfsKvp.TYPE_NAMES.getKey()));
+               if (typeNames.isEmpty()) {
+                       log.warn("FeatureAdapter " + featureAdapter.getClass() + " does not declare type names. Ignoring it...");
+                       return;
+               }
+
+               for (String tn : typeNames) {
+                       QName typeName = NamespaceUtils.parsePrefixedName(tn);
+                       featureAdapters.put(typeName, featureAdapter);
+               }
+       }
+
+       public void removeFeatureAdapter(FeatureAdapter featureAdapter, Map<String, Object> properties) {
+               List<String> typeNames = LangUtils.toStringList(properties.get(WfsKvp.TYPE_NAMES.getKey()));
+               if (!typeNames.isEmpty()) {
+                       // ignore if noe type name declared
+                       return;
+               }
+
+               for (String tn : typeNames) {
+                       QName typeName = NamespaceUtils.parsePrefixedName(tn);
+                       featureAdapters.remove(typeName);
+               }
+       }
+
        public void setContentRepository(ProvidedRepository contentRepository) {
                this.contentRepository = contentRepository;
        }