001: /* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved.
002: * This code is licensed under the GPL 2.0 license, available at the root
003: * application directory.
004: */
005: package org.vfny.geoserver.global;
006:
007: import java.io.IOException;
008: import java.util.LinkedList;
009: import java.util.List;
010: import java.util.Set;
011: import java.util.logging.Logger;
012:
013: import org.geoserver.feature.DefaultCRSFilterVisitor;
014: import org.geoserver.feature.ReprojectingFilterVisitor;
015: import org.geotools.data.DataSourceException;
016: import org.geotools.data.DataStore;
017: import org.geotools.data.DefaultQuery;
018: import org.geotools.data.FeatureListener;
019: import org.geotools.data.FeatureLocking;
020: import org.geotools.data.FeatureSource;
021: import org.geotools.data.FeatureStore;
022: import org.geotools.data.Query;
023: import org.geotools.data.crs.ForceCoordinateSystemFeatureResults;
024: import org.geotools.data.crs.ReprojectFeatureResults;
025: import org.geotools.factory.CommonFactoryFinder;
026: import org.geotools.feature.FeatureCollection;
027: import org.geotools.feature.FeatureType;
028: import org.geotools.feature.FeatureTypes;
029: import org.geotools.feature.GeometryAttributeType;
030: import org.geotools.feature.SchemaException;
031: import org.geotools.referencing.CRS;
032: import org.opengis.filter.Filter;
033: import org.opengis.filter.FilterFactory;
034: import org.opengis.filter.FilterFactory2;
035: import org.opengis.referencing.FactoryException;
036: import org.opengis.referencing.crs.CoordinateReferenceSystem;
037: import org.opengis.referencing.operation.OperationNotFoundException;
038: import org.opengis.referencing.operation.TransformException;
039:
040: import com.vividsolutions.jts.geom.Envelope;
041:
042: /**
043: * GeoServer wrapper for backend Geotools2 DataStore.
044: *
045: * <p>
046: * Support FeatureSource decorator for FeatureTypeInfo that takes care of
047: * mapping the FeatureTypeInfo's FeatureSource with the schema and definition
048: * query configured for it.
049: * </p>
050: *
051: * <p>
052: * Because GeoServer requires that attributes always be returned in the same
053: * order we need a way to smoothly inforce this. Could we use this class to do
054: * so?
055: * </p>
056: *
057: * @author Gabriel Rold�n
058: * @version $Id: GeoServerFeatureSource.java 8449 2008-02-25 18:14:20Z aaime $
059: */
060: public class GeoServerFeatureSource implements FeatureSource {
061: /** Shared package logger */
062: private static final Logger LOGGER = org.geotools.util.logging.Logging
063: .getLogger("org.vfny.geoserver.global");
064:
065: /** FeatureSource being served up */
066: protected FeatureSource source;
067:
068: /**
069: * GeoTools2 Schema information
070: *
071: * <p>
072: * Is this the same as source.getSchema() or is it used supply the order
073: * that GeoServer requires attributes to be returned in?
074: * </p>
075: */
076: protected FeatureType schema;
077:
078: /** Used to constrain the Feature made available to GeoServer. */
079: private Filter definitionQuery = Filter.INCLUDE;
080:
081: /** Geometries will be forced to this CRS (or null, if no forcing is needed) */
082: private CoordinateReferenceSystem declaredCRS;
083:
084: /** How to handle SRS */
085: private int srsHandling;
086:
087: /**
088: * Creates a new GeoServerFeatureSource object.
089: *
090: * @param source GeoTools2 FeatureSource
091: * @param schema FeatureType returned by this FeatureSource
092: * @param definitionQuery Filter used to limit results
093: * @param declaredCRS Geometries will be forced or projected to this CRS
094: */
095: GeoServerFeatureSource(FeatureSource source, FeatureType schema,
096: Filter definitionQuery,
097: CoordinateReferenceSystem declaredCRS, int srsHandling) {
098: this .source = source;
099: this .schema = schema;
100: this .definitionQuery = definitionQuery;
101: this .declaredCRS = declaredCRS;
102: this .srsHandling = srsHandling;
103:
104: if (this .definitionQuery == null) {
105: this .definitionQuery = Filter.INCLUDE;
106: }
107: }
108:
109: /**
110: * Factory that make the correct decorator for the provided featureSource.
111: *
112: * <p>
113: * This factory method is public and will be used to create all required
114: * subclasses. By comparison the constructors for this class have package
115: * visibiliy.
116: * </p>
117: *
118: * @param featureSource
119: * @param schema DOCUMENT ME!
120: * @param definitionQuery DOCUMENT ME!
121: * @param declaredCRS
122: *
123: * @return
124: */
125: public static GeoServerFeatureSource create(
126: FeatureSource featureSource, FeatureType schema,
127: Filter definitionQuery,
128: CoordinateReferenceSystem declaredCRS, int srsHandling) {
129: if (featureSource instanceof FeatureLocking) {
130: return new GeoServerFeatureLocking(
131: (FeatureLocking) featureSource, schema,
132: definitionQuery, declaredCRS, srsHandling);
133: } else if (featureSource instanceof FeatureStore) {
134: return new GeoServerFeatureStore(
135: (FeatureStore) featureSource, schema,
136: definitionQuery, declaredCRS, srsHandling);
137: }
138:
139: return new GeoServerFeatureSource(featureSource, schema,
140: definitionQuery, declaredCRS, srsHandling);
141: }
142:
143: /**
144: * Takes a query and adapts it to match re definitionQuery filter
145: * configured for a feature type.
146: *
147: * @param query Query against this DataStore
148: * @param schema TODO
149: *
150: * @return Query restricted to the limits of definitionQuery
151: *
152: * @throws IOException See DataSourceException
153: * @throws DataSourceException If query could not meet the restrictions of
154: * definitionQuery
155: */
156: protected Query makeDefinitionQuery(Query query, FeatureType schema)
157: throws IOException {
158: if ((query == Query.ALL) || query.equals(Query.ALL)) {
159: return query;
160: }
161:
162: try {
163: String[] propNames = extractAllowedAttributes(query, schema);
164: Filter filter = query.getFilter();
165: filter = makeDefinitionFilter(filter);
166:
167: DefaultQuery defQuery = new DefaultQuery(query);
168: defQuery.setFilter(filter);
169: defQuery.setPropertyNames(propNames);
170:
171: //set sort by
172: if (query.getSortBy() != null) {
173: defQuery.setSortBy(query.getSortBy());
174: }
175:
176: return defQuery;
177: } catch (Exception ex) {
178: throw new DataSourceException(
179: "Could not restrict the query to the definition criteria: "
180: + ex.getMessage(), ex);
181: }
182: }
183:
184: /**
185: * List of allowed attributes.
186: *
187: * <p>
188: * Creates a list of FeatureTypeInfo's attribute names based on the
189: * attributes requested by <code>query</code> and making sure they not
190: * contain any non exposed attribute.
191: * </p>
192: *
193: * <p>
194: * Exposed attributes are those configured in the "attributes" element of
195: * the FeatureTypeInfo's configuration
196: * </p>
197: *
198: * @param query User's origional query
199: * @param schema TODO
200: *
201: * @return List of allowed attribute types
202: */
203: private String[] extractAllowedAttributes(Query query,
204: FeatureType schema) {
205: String[] propNames = null;
206:
207: if (query.retrieveAllProperties()) {
208: propNames = new String[schema.getAttributeCount()];
209:
210: for (int i = 0; i < schema.getAttributeCount(); i++) {
211: propNames[i] = schema.getAttributeType(i).getName();
212: }
213: } else {
214: String[] queriedAtts = query.getPropertyNames();
215: int queriedAttCount = queriedAtts.length;
216: List allowedAtts = new LinkedList();
217:
218: for (int i = 0; i < queriedAttCount; i++) {
219: if (schema.getAttributeType(queriedAtts[i]) != null) {
220: allowedAtts.add(queriedAtts[i]);
221: } else {
222: LOGGER.info("queried a not allowed property: "
223: + queriedAtts[i]
224: + ". Ommitting it from query");
225: }
226: }
227:
228: propNames = (String[]) allowedAtts
229: .toArray(new String[allowedAtts.size()]);
230: }
231:
232: return propNames;
233: }
234:
235: /**
236: * If a definition query has been configured for the FeatureTypeInfo, makes
237: * and return a new Filter that contains both the query's filter and the
238: * layer's definition one, by logic AND'ing them.
239: *
240: * @param filter Origional user supplied Filter
241: *
242: * @return Filter adjusted to the limitations of definitionQuery
243: *
244: * @throws DataSourceException If the filter could not meet the limitations
245: * of definitionQuery
246: */
247: protected Filter makeDefinitionFilter(Filter filter)
248: throws DataSourceException {
249: Filter newFilter = filter;
250:
251: try {
252: if (definitionQuery != Filter.INCLUDE) {
253: FilterFactory ff = CommonFactoryFinder
254: .getFilterFactory(null);
255: newFilter = ff.and(definitionQuery, filter);
256: }
257: } catch (Exception ex) {
258: throw new DataSourceException(
259: "Can't create the definition filter", ex);
260: }
261:
262: return newFilter;
263: }
264:
265: /**
266: * Implement getDataStore.
267: *
268: * <p>
269: * Description ...
270: * </p>
271: *
272: * @return
273: *
274: * @see org.geotools.data.FeatureSource#getDataStore()
275: */
276: public DataStore getDataStore() {
277: return source.getDataStore();
278: }
279:
280: /**
281: * Implement addFeatureListener.
282: *
283: * <p>
284: * Description ...
285: * </p>
286: *
287: * @param listener
288: *
289: * @see org.geotools.data.FeatureSource#addFeatureListener(org.geotools.data.FeatureListener)
290: */
291: public void addFeatureListener(FeatureListener listener) {
292: source.addFeatureListener(listener);
293: }
294:
295: /**
296: * Implement removeFeatureListener.
297: *
298: * <p>
299: * Description ...
300: * </p>
301: *
302: * @param listener
303: *
304: * @see org.geotools.data.FeatureSource#removeFeatureListener(org.geotools.data.FeatureListener)
305: */
306: public void removeFeatureListener(FeatureListener listener) {
307: source.removeFeatureListener(listener);
308: }
309:
310: /**
311: * Implement getFeatures.
312: *
313: * <p>
314: * Description ...
315: * </p>
316: *
317: * @param query
318: *
319: * @return
320: *
321: * @throws IOException
322: *
323: * @see org.geotools.data.FeatureSource#getFeatures(org.geotools.data.Query)
324: */
325: public FeatureCollection getFeatures(Query query)
326: throws IOException {
327: Query reprojected = reprojectFilter(query);
328: Query newQuery = adaptQuery(reprojected, schema);
329:
330: CoordinateReferenceSystem targetCRS = query
331: .getCoordinateSystemReproject();
332: try {
333: //this is the raw "unprojected" feature collection
334: FeatureCollection fc = source.getFeatures(newQuery);
335:
336: return reprojectFeatureCollection(targetCRS, fc);
337: } catch (Exception e) {
338: throw new DataSourceException(e);
339: }
340: }
341:
342: private Query reprojectFilter(Query query) throws IOException {
343: FeatureType nativeFeatureType = source.getSchema();
344: final GeometryAttributeType geom = nativeFeatureType
345: .getDefaultGeometry();
346: // handle the geometryless case
347: if (geom == null)
348: return query;
349: try {
350: // default CRS: the CRS we can assume geometry and bbox elements in filter are
351: // that is, usually the declared one, but the native one in the leave case
352: CoordinateReferenceSystem defaultCRS = null;
353: // we need to reproject all bbox and geometries to a target crs, which is
354: // the native one usually, but it's the declared on in the force case (since in
355: // that case we completely ignore the native one)
356: CoordinateReferenceSystem targetCRS = null;
357: CoordinateReferenceSystem nativeCRS = geom
358: .getCoordinateSystem();
359: if (srsHandling == FeatureTypeInfo.FORCE) {
360: defaultCRS = declaredCRS;
361: targetCRS = declaredCRS;
362: nativeFeatureType = FeatureTypes.transform(
363: nativeFeatureType, declaredCRS);
364: } else if (srsHandling == FeatureTypeInfo.REPROJECT) {
365: defaultCRS = declaredCRS;
366: targetCRS = nativeCRS;
367: } else { // FeatureTypeInfo.LEAVE
368: defaultCRS = nativeCRS;
369: targetCRS = nativeCRS;
370: }
371:
372: // now we apply a default to all geometries and bbox in the filter
373: FilterFactory2 ff = CommonFactoryFinder
374: .getFilterFactory2(null);
375: DefaultCRSFilterVisitor defaultCRSVisitor = new DefaultCRSFilterVisitor(
376: ff, defaultCRS);
377: Filter defaultedFilter = (Filter) query.getFilter().accept(
378: defaultCRSVisitor, null);
379:
380: // and then we reproject all geometries so that the datastore receives
381: // them in the native projection system (or the forced one, in case of force)
382: ReprojectingFilterVisitor reprojectingVisitor = new ReprojectingFilterVisitor(
383: ff, nativeFeatureType);
384: Filter reprojectedFilter = (Filter) defaultedFilter.accept(
385: reprojectingVisitor, null);
386:
387: DefaultQuery reprojectedQuery = new DefaultQuery(query);
388: reprojectedQuery.setFilter(reprojectedFilter);
389: return reprojectedQuery;
390: } catch (Exception e) {
391: throw new DataSourceException(
392: "Had troubles handling filter reprojection...", e);
393: }
394: }
395:
396: /**
397: * Wraps feature collection as needed in order to respect srs handling and reprojection
398: * @param targetCRS
399: * @param fc
400: * @return
401: * @throws IOException
402: * @throws SchemaException
403: * @throws TransformException
404: * @throws OperationNotFoundException
405: * @throws FactoryException
406: */
407: protected FeatureCollection reprojectFeatureCollection(
408: CoordinateReferenceSystem targetCRS, FeatureCollection fc)
409: throws IOException, SchemaException, TransformException,
410: OperationNotFoundException, FactoryException {
411: if (fc.getSchema().getDefaultGeometry() == null) {
412: // reprojection and crs forcing do not make sense, bail out
413: return fc;
414: }
415: CoordinateReferenceSystem nativeCRS = fc.getSchema()
416: .getDefaultGeometry().getCoordinateSystem();
417:
418: if (nativeCRS == null) {
419: fc = new ForceCoordinateSystemFeatureResults(fc,
420: declaredCRS);
421: nativeCRS = declaredCRS;
422: } else if (srsHandling == FeatureTypeInfo.FORCE
423: && !nativeCRS.equals(declaredCRS)) {
424: fc = new ForceCoordinateSystemFeatureResults(fc,
425: declaredCRS);
426: nativeCRS = declaredCRS;
427: } else if (srsHandling == FeatureTypeInfo.REPROJECT
428: && targetCRS == null && !nativeCRS.equals(declaredCRS)) {
429: fc = new ReprojectFeatureResults(fc, declaredCRS);
430: }
431:
432: //was reproject specified as part of the query?
433: if (targetCRS != null) {
434: //reprojection is occuring
435: if (nativeCRS == null) {
436: //we do not know what the native crs which means we can
437: // not be sure if we should reproject or not... so we go
438: // ahead and reproject regardless
439: fc = new ReprojectFeatureResults(fc, targetCRS);
440: } else {
441: //only reproject if native != target
442: if (!CRS.equalsIgnoreMetadata(nativeCRS, targetCRS)) {
443: fc = new ReprojectFeatureResults(fc, targetCRS);
444: }
445: }
446: }
447: return fc;
448: }
449:
450: /**
451: * Transforms the query applying the definition query in this layer, removes reprojection
452: * since data stores cannot be trusted
453: * @param query
454: * @param schema TODO
455: * @return
456: * @throws IOException
457: */
458: protected Query adaptQuery(Query query, FeatureType schema)
459: throws IOException {
460: // if needed, reproject the filter to the native srs
461:
462: Query newQuery = makeDefinitionQuery(query, schema);
463:
464: // see if the CRS got xfered over
465: // a. old had a CRS, new doesnt
466: boolean requireXferCRS = (newQuery.getCoordinateSystem() == null)
467: && (query.getCoordinateSystem() != null);
468:
469: if ((newQuery.getCoordinateSystem() != null)
470: && (query.getCoordinateSystem() != null)) {
471: //b. both have CRS, but they're different
472: requireXferCRS = !(newQuery.getCoordinateSystem()
473: .equals(query.getCoordinateSystem()));
474: }
475:
476: if (requireXferCRS) {
477: //carry along the CRS
478: if (!(newQuery instanceof DefaultQuery)) {
479: newQuery = new DefaultQuery(newQuery);
480: }
481:
482: ((DefaultQuery) newQuery).setCoordinateSystem(query
483: .getCoordinateSystem());
484: }
485:
486: //JD: this is a huge hack... but its the only way to ensure that we
487: // we get what we ask for ... which is not reprojection, since
488: // datastores are unreliable in this aspect we dont know if they will
489: // reproject or not.
490: if (newQuery.getCoordinateSystemReproject() != null) {
491: ((DefaultQuery) newQuery)
492: .setCoordinateSystemReproject(null);
493: }
494: return newQuery;
495: }
496:
497: public FeatureCollection getFeatures(Filter filter)
498: throws IOException {
499: return getFeatures(new DefaultQuery(schema.getTypeName(),
500: filter));
501: }
502:
503: public FeatureCollection getFeatures() throws IOException {
504: return getFeatures(Query.ALL);
505: }
506:
507: /**
508: * Implement getSchema.
509: *
510: * <p>
511: * Description ...
512: * </p>
513: *
514: * @return
515: *
516: * @see org.geotools.data.FeatureSource#getSchema()
517: */
518: public FeatureType getSchema() {
519: return schema;
520: }
521:
522: /**
523: * Retrieves the total extent of this FeatureSource.
524: *
525: * <p>
526: * Please note this extent will reflect the provided definitionQuery.
527: * </p>
528: *
529: * @return Extent of this FeatureSource, or <code>null</code> if no
530: * optimizations exist.
531: *
532: * @throws IOException If bounds of definitionQuery
533: */
534: public Envelope getBounds() throws IOException {
535: // since CRS is at most forced, we don't need to change this code
536: if (definitionQuery == Filter.INCLUDE) {
537: return source.getBounds();
538: } else {
539: Query query = new DefaultQuery(getSchema().getTypeName(),
540: definitionQuery);
541:
542: return source.getBounds(query);
543: }
544: }
545:
546: /**
547: * Retrive the extent of the Query.
548: *
549: * <p>
550: * This method provides access to an optimized getBounds opperation. If no
551: * optimized opperation is available <code>null</code> will be returned.
552: * </p>
553: *
554: * <p>
555: * You may still make use of getFeatures( Query ).getCount() which will
556: * return the correct answer (even if it has to itterate through all the
557: * results to do so.
558: * </p>
559: *
560: * @param query User's query
561: *
562: * @return Extend of Query or <code>null</code> if no optimization is
563: * available
564: *
565: * @throws IOException If a problem is encountered with source
566: */
567: public Envelope getBounds(Query query) throws IOException {
568: // since CRS is at most forced, we don't need to change this code
569: try {
570: query = makeDefinitionQuery(query, schema);
571: } catch (IOException ex) {
572: return null;
573: }
574:
575: return source.getBounds(query);
576: }
577:
578: /**
579: * Adjust query and forward to source.
580: *
581: * <p>
582: * This method provides access to an optimized getCount opperation. If no
583: * optimized opperation is available <code>-1</code> will be returned.
584: * </p>
585: *
586: * <p>
587: * You may still make use of getFeatures( Query ).getCount() which will
588: * return the correct answer (even if it has to itterate through all the
589: * results to do so).
590: * </p>
591: *
592: * @param query User's query.
593: *
594: * @return Number of Features for Query, or -1 if no optimization is
595: * available.
596: */
597: public int getCount(Query query) {
598: try {
599: query = makeDefinitionQuery(query, schema);
600: } catch (IOException ex) {
601: return -1;
602: }
603:
604: try {
605: return source.getCount(query);
606: } catch (IOException e) {
607: return 0;
608: }
609: }
610:
611: public Set getSupportedHints() {
612: return source.getSupportedHints();
613: }
614: }
|