001    //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/branches/2.2_testing/src/org/deegree/model/feature/GMLFeatureAdapter.java $
002    /*----------------    FILE HEADER  ------------------------------------------
003    
004     This file is part of deegree.
005     Copyright (C) 2001-2008 by:
006     EXSE, Department of Geography, University of Bonn
007     http://www.giub.uni-bonn.de/deegree/
008     lat/lon GmbH
009     http://www.lat-lon.de
010    
011     This library is free software; you can redistribute it and/or
012     modify it under the terms of the GNU Lesser General Public
013     License as published by the Free Software Foundation; either
014     version 2.1 of the License, or (at your option) any later version.
015    
016     This library is distributed in the hope that it will be useful,
017     but WITHOUT ANY WARRANTY; without even the implied warranty of
018     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
019     Lesser General Public License for more details.
020    
021     You should have received a copy of the GNU Lesser General Public
022     License along with this library; if not, write to the Free Software
023     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
024    
025     Contact:
026    
027     Andreas Poth
028     lat/lon GmbH
029     Aennchenstraße 19
030     53177 Bonn
031     Germany
032     E-Mail: poth@lat-lon.de
033    
034     Prof. Dr. Klaus Greve
035     Department of Geography
036     University of Bonn
037     Meckenheimer Allee 166
038     53115 Bonn
039     Germany
040     E-Mail: greve@giub.uni-bonn.de
041     
042     ---------------------------------------------------------------------------*/
043    package org.deegree.model.feature;
044    
045    import java.io.ByteArrayInputStream;
046    import java.io.ByteArrayOutputStream;
047    import java.io.IOException;
048    import java.io.OutputStream;
049    import java.io.OutputStreamWriter;
050    import java.io.PrintWriter;
051    import java.math.BigDecimal;
052    import java.net.URI;
053    import java.sql.Timestamp;
054    import java.util.Calendar;
055    import java.util.Date;
056    import java.util.HashMap;
057    import java.util.HashSet;
058    import java.util.Iterator;
059    import java.util.Map;
060    import java.util.Set;
061    
062    import org.deegree.datatypes.QualifiedName;
063    import org.deegree.datatypes.Types;
064    import org.deegree.framework.log.ILogger;
065    import org.deegree.framework.log.LoggerFactory;
066    import org.deegree.framework.util.CharsetUtils;
067    import org.deegree.framework.util.StringTools;
068    import org.deegree.framework.util.TimeTools;
069    import org.deegree.framework.xml.DOMPrinter;
070    import org.deegree.framework.xml.XMLException;
071    import org.deegree.framework.xml.XMLFragment;
072    import org.deegree.framework.xml.XMLTools;
073    import org.deegree.io.datastore.schema.MappedFeaturePropertyType;
074    import org.deegree.io.datastore.schema.MappedFeatureType;
075    import org.deegree.model.feature.schema.FeatureType;
076    import org.deegree.model.feature.schema.PropertyType;
077    import org.deegree.model.spatialschema.Envelope;
078    import org.deegree.model.spatialschema.GMLGeometryAdapter;
079    import org.deegree.model.spatialschema.Geometry;
080    import org.deegree.model.spatialschema.GeometryException;
081    import org.deegree.ogcbase.CommonNamespaces;
082    import org.w3c.dom.Element;
083    import org.xml.sax.SAXException;
084    
085    /**
086     * Exports feature instances to their GML representation.
087     * <p>
088     * Has support for XLink output and to disable XLink output (which is generally not feasible).
089     * <p>
090     * Also responsible for "xlinking" features: if a feature occurs several times in a feature
091     * collection, it must be exported only once - all other occurences must use xlink-attributes in the
092     * surrounding property element to reference the feature.
093     * 
094     * TODO Handle FeatureCollections like ordinary Features (needs changes in feature model).
095     * 
096     * TODO Separate cycle check (for suppressXLinkOutput).
097     * 
098     * TODO Use a more straight-forward approach to export DOM representations.
099     * 
100     * TODO Handle multiple application schemas (in xsi:schemaLocation attribute).
101     * 
102     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider </a>
103     * @author last edited by: $Author: apoth $
104     * 
105     * @version $Revision: 9343 $, $Date: 2007-12-27 14:30:32 +0100 (Do, 27 Dez 2007) $
106     */
107    public class GMLFeatureAdapter {
108    
109        private static final ILogger LOG = LoggerFactory.getLogger( GMLFeatureAdapter.class );
110    
111        private static final String WFS_SCHEMA_BINDING = "http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.1.0/wfs.xsd";
112    
113        // values: feature ids of already exported features (for XLinks)
114        private Set<String> exportedFeatures = new HashSet<String>();
115    
116        // values: feature ids of all (sub-) features in a feature (to find cyclic features)
117        private Set<String> localFeatures = new HashSet<String>();
118    
119        private boolean suppressXLinkOutput;
120    
121        private String schemaURL;
122    
123        // marks if namespace bindings have been appended already
124        private boolean nsBindingsExported;
125    
126        /**
127         * Creates a new <code>GMLFeatureAdapter</code> instance with enabled XLink output.
128         */
129        public GMLFeatureAdapter() {
130            this.suppressXLinkOutput = false;
131        }
132    
133        /**
134         * Creates a new <code>GMLFeatureAdapter</code> instance with enabled XLink output and schema
135         * reference.
136         * 
137         * @param schemaURL
138         *            URL of schema document (used as xsi:schemaLocation attribute in XML output)
139         */
140        public GMLFeatureAdapter( String schemaURL ) {
141            this.suppressXLinkOutput = false;
142            if ( schemaURL != null ) {
143                this.schemaURL = StringTools.replace( schemaURL, "&", "&amp;", true );
144            }
145        }
146    
147        /**
148         * Creates a new instance <code>GMLFeatureAdapter</code> with configurable XLink output.
149         * 
150         * @param suppressXLinkOutput
151         *            set to true, if no XLinks shall be used
152         */
153        public GMLFeatureAdapter( boolean suppressXLinkOutput ) {
154            this.suppressXLinkOutput = suppressXLinkOutput;
155        }
156    
157        /**
158         * Creates a new instance <code>GMLFeatureAdapter</code> with configurable XLink output.
159         * 
160         * @param suppressXLinkOutput
161         *            set to true, if no XLinks shall be used
162         * @param schemaURL
163         *            URL of schema document (used as xsi:schemaLocation attribute in XML output)
164         */
165        public GMLFeatureAdapter( boolean suppressXLinkOutput, String schemaURL ) {
166            this.suppressXLinkOutput = suppressXLinkOutput;
167            if ( schemaURL != null ) {
168                this.schemaURL = StringTools.replace( schemaURL, "&", "&amp;", true );
169            }
170        }
171    
172        /**
173         * Appends the DOM representation of the given feature to the also given <code>Node</code>.
174         * <p>
175         * TODO do this a better way (append nodes directly without serializing to string and parsing it
176         * again)
177         * 
178         * @param root
179         * @param feature
180         * @throws FeatureException
181         * @throws IOException
182         * @throws SAXException
183         */
184        public void append( Element root, Feature feature )
185                                throws FeatureException, IOException, SAXException {
186    
187            GMLFeatureDocument doc = export( feature );
188            XMLTools.insertNodeInto( doc.getRootElement(), root );
189        }
190    
191        /**
192         * Export a <code>Feature</code> to it's XML representation.
193         * 
194         * @param feature
195         *            feature to export
196         * @return XML representation of feature
197         * @throws IOException
198         * @throws FeatureException
199         * @throws XMLException
200         * @throws SAXException
201         */
202        public GMLFeatureDocument export( Feature feature )
203                                throws IOException, FeatureException, XMLException, SAXException {
204    
205            ByteArrayOutputStream bos = new ByteArrayOutputStream( 20000 );
206            export( feature, bos );
207            ByteArrayInputStream bis = new ByteArrayInputStream( bos.toByteArray() );
208            bos.close();
209    
210            GMLFeatureDocument doc = new GMLFeatureDocument();
211            doc.load( bis, XMLFragment.DEFAULT_URL );
212            return doc;
213        }
214    
215        /**
216         * Appends the DOM representation of the given <code>FeatureCollection</code> to the also
217         * given <code>Node</code>.
218         * <p>
219         * TODO do this a better way (append nodes directly without serializing to string and parsing it
220         * again)
221         * 
222         * @param root
223         * @param fc
224         * @throws FeatureException
225         * @throws IOException
226         * @throws SAXException
227         */
228        public void append( Element root, FeatureCollection fc )
229                                throws FeatureException, IOException, SAXException {
230    
231            GMLFeatureCollectionDocument doc = export( fc );
232            XMLTools.insertNodeInto( doc.getRootElement(), root );
233        }
234    
235        /**
236         * Export a <code>FeatureCollection</code> to it's XML representation.
237         * 
238         * @param fc
239         *            feature collection
240         * @return XML representation of feature collection
241         * @throws IOException
242         * @throws FeatureException
243         * @throws XMLException
244         * @throws SAXException
245         */
246        public GMLFeatureCollectionDocument export( FeatureCollection fc )
247                                throws IOException, FeatureException, XMLException, SAXException {
248    
249            ByteArrayOutputStream bos = new ByteArrayOutputStream( 20000 );
250            export( fc, bos );
251            ByteArrayInputStream bis = new ByteArrayInputStream( bos.toByteArray() );
252            bos.close();
253    
254            GMLFeatureCollectionDocument doc = new GMLFeatureCollectionDocument();
255            doc.load( bis, XMLFragment.DEFAULT_URL );
256            return doc;
257        }
258    
259        /**
260         * Exports an instance of a <code>FeatureCollection</code> to the passed
261         * <code>OutputStream</code> formatted as GML. Uses the deegree system character set for the
262         * XML header encoding information.
263         * 
264         * @param fc
265         *            feature collection to export
266         * @param os
267         *            output stream to write to
268         * 
269         * @throws IOException
270         * @throws FeatureException
271         */
272        public void export( FeatureCollection fc, OutputStream os )
273                                throws IOException, FeatureException {
274            export( fc, os, CharsetUtils.getSystemCharset() );
275        }
276    
277        /**
278         * Exports a <code>FeatureCollection</code> instance to the passed <code>OutputStream</code>
279         * formatted as GML.
280         * 
281         * @param fc
282         *            feature collection to export
283         * @param os
284         *            output stream to write to
285         * @param charsetName
286         *            name of the used charset/encoding (for the XML header)
287         * 
288         * @throws IOException
289         * @throws FeatureException
290         */
291        public void export( FeatureCollection fc, OutputStream os, String charsetName )
292                                throws IOException, FeatureException {
293    
294            PrintWriter pw = new PrintWriter( new OutputStreamWriter( os, charsetName ) );
295            pw.println( "<?xml version=\"1.0\" encoding=\"" + charsetName + "\"?>" );
296            if ( fc instanceof FeatureTupleCollection ) {
297                exportTupleCollection( (FeatureTupleCollection) fc, pw );
298            } else {
299                exportRootCollection( fc, pw );
300            }
301            pw.close();
302        }
303    
304        /**
305         * Exports a <code>FeatureCollection</code> instance to the passed <code>OutputStream</code>
306         * formatted as GML.
307         * 
308         * @param fc
309         *            feature collection to print/export
310         * @param pw
311         *            target of the printing/export
312         * @throws FeatureException
313         */
314        private void exportRootCollection( FeatureCollection fc, PrintWriter pw )
315                                throws FeatureException {
316    
317            Set<Feature> additionalRootLevelFeatures = determineAdditionalRootLevelFeatures( fc );
318            if ( this.suppressXLinkOutput && additionalRootLevelFeatures.size() > 0 ) {
319                String msg = Messages.getString( "ERROR_REFERENCE_TYPE" );
320                throw new FeatureException( msg );
321            }
322    
323            if ( fc.getId() != null && !"".equals( fc.getId() ) ) {
324                this.exportedFeatures.add( fc.getId() );
325            }
326    
327            // open the feature collection element
328            pw.print( "<" );
329            pw.print( fc.getName().getPrefixedName() );
330    
331            // hack to correct the "numberOfFeatures" attribute (can not be set in any case, because
332            // sometimes (resultType="hits") the collection contains no features at all -- but only the
333            // attribute "numberOfFeatures"
334            if ( fc.size() > 0 ) {
335                int hackedFeatureCount = fc.size() + additionalRootLevelFeatures.size();
336                fc.setAttribute( "numberOfFeatures", "" + hackedFeatureCount );
337            }
338    
339            Map<String, String> attributes = fc.getAttributes();
340            for ( Iterator iterator = attributes.keySet().iterator(); iterator.hasNext(); ) {
341                String name = (String) iterator.next();
342                String value = attributes.get( name );
343                pw.print( ' ' );
344                pw.print( name );
345                pw.print( "='" );
346                pw.print( value );
347                pw.print( "'" );
348            }
349    
350            // determine and add namespace bindings
351            Map<String, URI> nsBindings = determineUsedNSBindings( fc );
352            nsBindings.put( "gml", CommonNamespaces.GMLNS );
353            nsBindings.put( "xlink", CommonNamespaces.XLNNS );
354            if ( this.schemaURL != null ) {
355                nsBindings.put( "xsi", CommonNamespaces.XSINS );
356            }
357            appendNSBindings( nsBindings, pw );
358    
359            // add schema reference (if available)
360            if ( this.schemaURL != null && fc.size() > 0 ) {
361                pw.print( " xsi:schemaLocation=\"" + fc.getFeature( 0 ).getName().getNamespace() + " " );
362                pw.print( this.schemaURL + " " );
363                pw.print( WFS_SCHEMA_BINDING + "\"" );
364            }
365            pw.print( '>' );
366    
367            Envelope env = null;
368            try {
369                env = fc.getBoundedBy();
370            } catch ( GeometryException e ) {
371                // omit gml:boundedBy-element if featureCollection contains features
372                // with different SRS (and their envelopes cannot be merged)
373            }
374            if ( env != null ) {
375                pw.print( "<gml:boundedBy><gml:Envelope" );
376                if ( env.getCoordinateSystem() != null ) {
377                    pw.print( " srsName='" + env.getCoordinateSystem().getPrefixedName() + "'" );
378                }
379                pw.print( "><gml:pos srsDimension='2'>" );
380                pw.print( env.getMin().getX() );
381                pw.print( ' ' );
382                pw.print( env.getMin().getY() );
383                pw.print( "</gml:pos><gml:pos srsDimension='2'>" );
384                pw.print( env.getMax().getX() );
385                pw.print( ' ' );
386                pw.print( env.getMax().getY() );
387                pw.print( "</gml:pos></gml:Envelope></gml:boundedBy>" );
388            }
389    
390            // export all contained features
391            for ( int i = 0; i < fc.size(); i++ ) {
392                Feature feature = fc.getFeature( i );
393                String fid = feature.getId();
394                if ( fid != null && !fid.equals( "" ) && this.exportedFeatures.contains( fid ) && !this.suppressXLinkOutput ) {
395                    pw.print( "<gml:featureMember xlink:href=\"#" );
396                    pw.print( fid );
397                    pw.print( "\"/>" );
398                } else {
399                    pw.print( "<gml:featureMember>" );
400                    export( feature, pw );
401                    pw.print( "</gml:featureMember>" );
402                }
403            }
404    
405            // export all additional root level features
406            for ( Feature feature : additionalRootLevelFeatures ) {
407                String fid = feature.getId();
408                if ( fid != null && !fid.equals( "" ) && this.exportedFeatures.contains( fid ) ) {
409                    pw.print( "<gml:featureMember xlink:href=\"#" );
410                    pw.print( fid );
411                    pw.print( "\"/>" );
412                } else {
413                    pw.print( "<gml:featureMember>" );
414                    export( feature, pw );
415                    pw.print( "</gml:featureMember>" );
416                }
417            }
418    
419            // close the feature collection element
420            pw.print( "</" );
421            pw.print( fc.getName().getPrefixedName() );
422            pw.print( '>' );
423        }
424    
425        /**
426         * Determines features that don't originally belong to the root level of the given
427         * <code>FeatureCollection</code>, but which need to be put there in the output, because they
428         * are subfeatures inside of properties that have the content type "gml:ReferenceType".
429         * 
430         * @param fc
431         * @return features to be added to the root level
432         */
433        private Set<Feature> determineAdditionalRootLevelFeatures( FeatureCollection fc ) {
434    
435            Set<Feature> rootFeatures = new HashSet<Feature>( fc.size() );
436            for ( int i = 0; i < fc.size(); i++ ) {
437                rootFeatures.add( fc.getFeature( i ) );
438            }
439            Set<Feature> additionalRootFeatures = new HashSet<Feature>();
440            Set<Feature> checkedFeatures = new HashSet<Feature>();
441            for ( int i = 0; i < fc.size(); i++ ) {
442                Feature feature = fc.getFeature( i );
443                determineAdditionalRootLevelFeatures( feature, additionalRootFeatures, rootFeatures, checkedFeatures );
444            }
445            return additionalRootFeatures;
446        }
447    
448        /**
449         * Determines features that don't originally belong to the root level of the given
450         * <code>FeatureCollection</code>, but which need to be put there in the output, because they
451         * are subfeatures inside of properties that have the content type "gml:ReferenceType".
452         * 
453         * @param fc
454         * @param additionalFeatures
455         *            to be added to the root level
456         * @param rootFeatures
457         *            to be added to the root level
458         * @param checkedFeatures
459         *            features that have already been checked
460         */
461        private void determineAdditionalRootLevelFeatures( Feature feature, Set<Feature> additionalFeatures,
462                                                           Set<Feature> rootFeatures, Set<Feature> checkedFeatures ) {
463            for ( FeatureProperty property : feature.getProperties() ) {
464                Object value = property.getValue();
465                if ( value instanceof Feature ) {
466                    Feature subFeature = (Feature) value;
467                    if ( !checkedFeatures.contains( subFeature ) ) {
468                        if ( feature.getFeatureType() != null && feature.getFeatureType() instanceof MappedFeatureType ) {
469                            MappedFeatureType ft = (MappedFeatureType) feature.getFeatureType();
470                            MappedFeaturePropertyType pt = (MappedFeaturePropertyType) ft.getProperty( property.getName() );
471                            assert pt != null;
472                            if ( pt.isReferenceType() && !rootFeatures.contains( subFeature ) ) {
473                                additionalFeatures.add( (Feature) value );
474                            }
475                        }
476                        checkedFeatures.add( subFeature );
477                        determineAdditionalRootLevelFeatures( subFeature, additionalFeatures, rootFeatures, checkedFeatures );
478                    }
479                }
480            }
481        }
482    
483        /**
484         * Exports a {@link FeatureTupleCollection} instance to the passed {@link OutputStream}
485         * formatted as GML.
486         * 
487         * @param fc
488         *            feature tuple collection to print/export
489         * @param pw
490         *            target of the printing/export
491         * @throws FeatureException
492         */
493        private void exportTupleCollection( FeatureTupleCollection fc, PrintWriter pw )
494                                throws FeatureException {
495    
496            if ( fc.getId() != null && !"".equals( fc.getId() ) ) {
497                this.exportedFeatures.add( fc.getId() );
498            }
499    
500            // open the feature collection element
501            pw.print( "<" );
502            pw.print( fc.getName().getPrefixedName() );
503    
504            Map<String, String> attributes = fc.getAttributes();
505            for ( Iterator iterator = attributes.keySet().iterator(); iterator.hasNext(); ) {
506                String name = (String) iterator.next();
507                String value = attributes.get( name );
508                pw.print( ' ' );
509                pw.print( name );
510                pw.print( "='" );
511                pw.print( value );
512                pw.print( "'" );
513            }
514    
515            // determine and add namespace bindings
516            Map<String, URI> nsBindings = determineUsedNSBindings( fc );
517            nsBindings.put( "gml", CommonNamespaces.GMLNS );
518            nsBindings.put( "xlink", CommonNamespaces.XLNNS );
519            if ( this.schemaURL != null ) {
520                nsBindings.put( "xsi", CommonNamespaces.XSINS );
521            }
522            appendNSBindings( nsBindings, pw );
523    
524            // add schema reference (if available)
525            if ( this.schemaURL != null && fc.size() > 0 ) {
526                pw.print( " xsi:schemaLocation=\"" + fc.getTuple( 0 )[0].getName().getNamespace() + " " );
527                pw.print( this.schemaURL + " " );
528                pw.print( WFS_SCHEMA_BINDING + "\"" );
529            }
530            pw.print( '>' );
531    
532            Envelope env = null;
533            try {
534                env = fc.getBoundedBy();
535            } catch ( GeometryException e ) {
536                // omit gml:boundedBy-element if featureCollection contains features
537                // with different SRS (and their envelopes cannot be merged)
538            }
539            if ( env != null ) {
540                pw.print( "<gml:boundedBy><gml:Envelope" );
541                if ( env.getCoordinateSystem() != null ) {
542                    pw.print( " srsName='" + env.getCoordinateSystem().getPrefixedName() + "'" );
543                }
544                pw.print( "><gml:pos srsDimension='2'>" );
545                pw.print( env.getMin().getX() );
546                pw.print( ' ' );
547                pw.print( env.getMin().getY() );
548                pw.print( "</gml:pos><gml:pos srsDimension='2'>" );
549                pw.print( env.getMax().getX() );
550                pw.print( ' ' );
551                pw.print( env.getMax().getY() );
552                pw.print( "</gml:pos></gml:Envelope></gml:boundedBy>" );
553            }
554    
555            // export all contained feature tuples
556            for ( int i = 0; i < fc.numTuples(); i++ ) {
557                Feature[] features = fc.getTuple( i );
558                pw.print( "<gml:featureTuple>" );
559                for ( Feature feature : features ) {
560                    export( feature, pw );
561                }
562                pw.print( "</gml:featureTuple>" );
563            }
564    
565            // close the feature collection element
566            pw.print( "</" );
567            pw.print( fc.getName().getPrefixedName() );
568            pw.print( '>' );
569        }
570    
571        /**
572         * Determines the namespace bindings that are used in the feature collection.
573         * <p>
574         * NOTE: Currently only the bindings for the feature collection's root element and the contained
575         * features are considered. If a subfeature uses another bindings, this binding will be missing
576         * in the XML.
577         * 
578         * @param fc
579         *            feature collection
580         * @return the namespace bindings.
581         */
582        private Map<String, URI> determineUsedNSBindings( FeatureCollection fc ) {
583    
584            Map<String, URI> nsBindings = new HashMap<String, URI>();
585    
586            // process feature collection element
587            QualifiedName name = fc.getName();
588            nsBindings.put( name.getPrefix(), name.getNamespace() );
589    
590            if ( fc instanceof FeatureTupleCollection ) {
591                // process contained feature tuples
592                FeatureTupleCollection ftc = (FeatureTupleCollection) fc;
593                for ( int i = 0; i < ftc.numTuples(); i++ ) {
594                    Feature[] features = ftc.getTuple( i );
595                    for ( Feature feature : features ) {
596                        name = feature.getName();
597                        nsBindings.put( name.getPrefix(), name.getNamespace() );
598                    }
599                }
600            } else {
601                // process contained features
602                for ( int i = 0; i < fc.size(); i++ ) {
603                    name = fc.getFeature( i ).getName();
604                    nsBindings.put( name.getPrefix(), name.getNamespace() );
605                }
606            }
607    
608            return nsBindings;
609        }
610    
611        /**
612         * Determines the namespace bindings that are used in the feature.
613         * <p>
614         * NOTE: Currently only the bindings for the feature's root element and the contained features
615         * are considered. If a subfeature uses another bindings, this binding will be missing in the
616         * XML.
617         * 
618         * @param fc
619         *            feature
620         * @return the namespace bindings
621         */
622        private Map<String, URI> determineUsedNSBindings( Feature feature ) {
623    
624            Map<String, URI> nsBindings = new HashMap<String, URI>();
625    
626            // process feature element
627            QualifiedName name = feature.getName();
628            nsBindings.put( name.getPrefix(), name.getNamespace() );
629    
630            return nsBindings;
631        }
632    
633        /**
634         * Appends the given namespace bindings to the PrintWriter.
635         * 
636         * @param bindings
637         *            namespace bindings to append
638         * @param pw
639         *            PrintWriter to write to
640         */
641        private void appendNSBindings( Map<String, URI> bindings, PrintWriter pw ) {
642    
643            Iterator<String> prefixIter = bindings.keySet().iterator();
644            while ( prefixIter.hasNext() ) {
645                String prefix = prefixIter.next();
646                URI nsURI = bindings.get( prefix );
647                pw.print( " xmlns:" );
648                pw.print( prefix );
649                pw.print( "=\"" );
650                pw.print( nsURI );
651                pw.print( '\"' );
652            }
653            this.nsBindingsExported = true;
654        }
655    
656        /**
657         * Exports an instance of a <code>Feature</code> to the passed <code>OutputStream</code>
658         * formatted as GML. Uses the deegree system character set for the XML header encoding
659         * information.
660         * 
661         * @param feature
662         *            feature to export
663         * @param os
664         *            output stream to write to
665         * 
666         * @throws IOException
667         * @throws FeatureException
668         */
669        public void export( Feature feature, OutputStream os )
670                                throws IOException, FeatureException {
671            export( feature, os, CharsetUtils.getSystemCharset() );
672        }
673    
674        /**
675         * Exports a <code>Feature</code> instance to the passed <code>OutputStream</code> formatted
676         * as GML.
677         * 
678         * @param feature
679         *            feature to export
680         * @param os
681         *            output stream to write to
682         * @param charsetName
683         *            name of the used charset/encoding (for the XML header)
684         * @throws IOException
685         * @throws FeatureException
686         */
687        public void export( Feature feature, OutputStream os, String charsetName )
688                                throws IOException, FeatureException {
689    
690            PrintWriter pw = new PrintWriter( new OutputStreamWriter( os, charsetName ) );
691            pw.println( "<?xml version=\"1.0\" encoding=\"" + charsetName + "\"?>" );
692            export( feature, pw );
693            pw.close();
694        }
695    
696        /**
697         * Exports a <code>Feature</code> instance to the passed <code>PrintWriter</code> as GML.
698         * 
699         * @param feature
700         *            feature to export
701         * @param pw
702         *            PrintWriter to write to
703         * @throws FeatureException
704         */
705        private void export( Feature feature, PrintWriter pw )
706                                throws FeatureException {
707    
708            QualifiedName ftName = feature.getName();
709            String fid = feature.getId();
710    
711            if ( this.suppressXLinkOutput && fid != null && !"".equals( fid ) ) {
712                if ( this.localFeatures.contains( fid ) ) {
713                    String msg = Messages.format( "ERROR_CYLIC_FEATURE", fid );
714                    throw new FeatureException( msg );
715                }
716                this.localFeatures.add( fid );
717            }
718    
719            // open feature element (add gml:id attribute if feature has an id)
720            pw.print( '<' );
721            pw.print( ftName.getPrefixedName() );
722            if ( fid != null ) {
723                this.exportedFeatures.add( fid );
724                pw.print( " gml:id=\"" );
725                pw.print( fid );
726                pw.print( '\"' );
727            }
728    
729            // determine and add namespace bindings
730            if ( !this.nsBindingsExported ) {
731                Map<String, URI> nsBindings = determineUsedNSBindings( feature );
732                nsBindings.put( "gml", CommonNamespaces.GMLNS );
733                nsBindings.put( "xlink", CommonNamespaces.XLNNS );
734                if ( this.schemaURL != null ) {
735                    nsBindings.put( "xsi", CommonNamespaces.XSINS );
736                }
737                appendNSBindings( nsBindings, pw );
738            }
739    
740            pw.print( '>' );
741    
742            try {
743                Envelope env = null;
744                if ( ( env = feature.getBoundedBy() ) != null ) {
745                    pw.print( "<gml:boundedBy><gml:Envelope" );
746                    if ( env.getCoordinateSystem() != null ) {
747                        pw.print( " srsName='" + env.getCoordinateSystem().getPrefixedName() + "'" );
748                    }
749                    pw.print( "><gml:pos srsDimension='2'>" );
750                    pw.print( env.getMin().getX() );
751                    pw.print( ' ' );
752                    pw.print( env.getMin().getY() );
753                    pw.print( "</gml:pos><gml:pos srsDimension=\"2\">" );
754                    pw.print( env.getMax().getX() );
755                    pw.print( ' ' );
756                    pw.print( env.getMax().getY() );
757                    pw.print( "</gml:pos></gml:Envelope></gml:boundedBy>" );
758                }
759            } catch ( GeometryException e ) {
760                LOG.logError( e.getMessage(), e );
761            }
762    
763            // export all properties of the feature
764            FeatureProperty[] properties = feature.getProperties();
765            for ( int i = 0; i < properties.length; i++ ) {
766                if ( properties[i] != null && properties[i].getValue() != null ) {
767                    exportProperty( feature, properties[i], pw );
768                }
769            }
770    
771            // close feature element
772            pw.print( "</" );
773            pw.print( ftName.getPrefixedName() );
774            pw.println( '>' );
775    
776            if ( this.suppressXLinkOutput || fid != null ) {
777                this.localFeatures.remove( fid );
778            }
779        }
780    
781        /**
782         * Exports a <code>FeatureProperty</code> instance to the passed <code>PrintWriter</code> as
783         * GML.
784         * 
785         * @param feature
786         *            feature that the property belongs to
787         * @param property
788         *            property to export
789         * @param pw
790         *            PrintWriter to write to
791         * @throws FeatureException
792         */
793        private void exportProperty( Feature feature, FeatureProperty property, PrintWriter pw )
794                                throws FeatureException {
795    
796            QualifiedName propertyName = property.getName();
797            Object value = property.getValue();
798    
799            if ( value instanceof Feature ) {
800                Feature subfeature = (Feature) value;
801    
802                boolean isReferenceType = false;
803                if ( feature.getFeatureType() != null && feature.getFeatureType() instanceof MappedFeatureType ) {
804                    MappedFeatureType ft = (MappedFeatureType) feature.getFeatureType();
805                    MappedFeaturePropertyType pt = (MappedFeaturePropertyType) ft.getProperty( property.getName() );
806                    assert pt != null;
807                    isReferenceType = pt.isReferenceType();
808                }
809    
810                if ( isReferenceType || ( exportedFeatures.contains( subfeature.getId() ) && !this.suppressXLinkOutput ) ) {
811                    pw.print( '<' );
812                    pw.print( propertyName.getPrefixedName() );
813                    pw.print( " xlink:href=\"#" );
814                    pw.print( subfeature.getId() );
815                    pw.print( "\"/>" );
816                } else {
817                    pw.print( '<' );
818                    pw.print( propertyName.getPrefixedName() );
819                    pw.print( '>' );
820                    exportPropertyValue( subfeature, pw );
821                    pw.print( "</" );
822                    pw.print( propertyName.getPrefixedName() );
823                    pw.print( '>' );
824                }
825            } else {
826                pw.print( '<' );
827                pw.print( propertyName.getPrefixedName() );
828                pw.print( '>' );
829                if ( value != null ) {
830                    FeatureType ft = feature.getFeatureType();
831                    PropertyType pt = ft.getProperty( property.getName() );
832                    if ( pt.getType() == Types.ANYTYPE ) {
833                        pw.print( value );
834                    } else {
835                        exportPropertyValue( value, pw );
836                    }
837                }
838                pw.print( "</" );
839                pw.print( propertyName.getPrefixedName() );
840                pw.print( '>' );
841            }
842        }
843    
844        /**
845         * Exports the value of a property to the passed <code>PrintWriter</code> as GML.
846         * 
847         * TODO make available and use property type information to determine correct output format
848         * (e.g. xs:date, xs:time, xs:dateTime are all represented using Date objects and cannot be
849         * differentiated at the moment)
850         * 
851         * @param value
852         *            property value to export
853         * @param pw
854         *            PrintWriter to write to
855         * @throws FeatureException
856         */
857        private void exportPropertyValue( Object value, PrintWriter pw )
858                                throws FeatureException {
859            if ( value instanceof Feature ) {
860                export( (Feature) value, pw );
861            } else if ( value instanceof Feature[] ) {
862                Feature[] features = (Feature[]) value;
863                for ( int i = 0; i < features.length; i++ ) {
864                    export( features[i], pw );
865                }
866            } else if ( value instanceof Envelope ) {
867                exportEnvelope( (Envelope) value, pw );
868            } else if ( value instanceof FeatureCollection ) {
869                export( (FeatureCollection) value, pw );
870            } else if ( value instanceof Geometry ) {
871                exportGeometry( (Geometry) value, pw );
872            } else if ( value instanceof Date ) {
873    
874                // TODO: use (currently unavailable) property type information to determine correct
875                // output format (e.g. xs:date, xs:time, xs:dateTime are all represented using Date
876                // objects and cannot be differentiated at the moment)
877    
878                // pw.print( ( (Date) value ).toString() );
879                pw.print( TimeTools.getISOFormattedTime( (Date) value ) );
880            } else if ( value instanceof Calendar ) {
881                pw.print( TimeTools.getISOFormattedTime( (Calendar) value ) );
882            } else if ( value instanceof Timestamp ) {
883                pw.print( TimeTools.getISOFormattedTime( (Timestamp) value ) );
884            } else if ( value instanceof java.sql.Date ) {
885                pw.print( TimeTools.getISOFormattedTime( (java.sql.Date) value ) );
886            } else if ( value instanceof Integer || value instanceof Long || value instanceof Float
887                        || value instanceof Double || value instanceof BigDecimal ) {
888                pw.print( value.toString() );
889            } else if ( value instanceof String ) {
890                StringBuffer sb = DOMPrinter.validateCDATA( (String) value );
891                pw.print( sb );
892            } else if ( value instanceof Boolean ) {
893                pw.print( value );
894            } else {
895                LOG.logInfo( "Unhandled property class '" + value.getClass() + "' in GMLFeatureAdapter." );
896                StringBuffer sb = DOMPrinter.validateCDATA( value.toString() );
897                pw.print( sb );
898            }
899        }
900    
901        /**
902         * prints the passed geometry to the also passed PrintWriter formatted as GML
903         * 
904         * @param geo
905         *            geometry to print/extport
906         * @param pw
907         *            target of the printing/export
908         * @throws FeatureException
909         */
910        private void exportGeometry( Geometry geo, PrintWriter pw )
911                                throws FeatureException {
912            try {
913                pw.print( GMLGeometryAdapter.export( geo ) );
914            } catch ( Exception e ) {
915                LOG.logError( "", e );
916                throw new FeatureException( "Could not export geometry to GML: " + e.getMessage(), e );
917            }
918        }
919    
920        /**
921         * prints the passed geometry to the also passed PrintWriter formatted as GML
922         * 
923         * @param geo
924         *            geometry to print/extport
925         * @param pw
926         *            target of the printing/export
927         * @throws FeatureException
928         */
929        private void exportEnvelope( Envelope geo, PrintWriter pw )
930                                throws FeatureException {
931            try {
932                pw.print( GMLGeometryAdapter.exportAsBox( geo ) );
933            } catch ( Exception e ) {
934                throw new FeatureException( "Could not export envelope to GML: " + e.getMessage(), e );
935            }
936        }
937    }