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