//$Header: /home/deegree/jail/deegreerepository/deegree/src/org/deegree/model/feature/GMLFeatureAdapter.java,v 1.73 2006/11/09 17:45:53 mschneider Exp $ /*---------------- FILE HEADER ------------------------------------------ This file is part of deegree. Copyright (C) 2001-2006 by: EXSE, Department of Geography, University of Bonn http://www.giub.uni-bonn.de/deegree/ lat/lon GmbH http://www.lat-lon.de This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Contact: Andreas Poth lat/lon GmbH Aennchenstraße 19 53177 Bonn Germany E-Mail: poth@lat-lon.de Prof. Dr. Klaus Greve Department of Geography University of Bonn Meckenheimer Allee 166 53115 Bonn Germany E-Mail: greve@giub.uni-bonn.de ---------------------------------------------------------------------------*/ package org.deegree.model.feature; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.math.BigDecimal; import java.net.URI; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import org.deegree.datatypes.QualifiedName; import org.deegree.framework.log.ILogger; import org.deegree.framework.log.LoggerFactory; import org.deegree.framework.util.CharsetUtils; import org.deegree.framework.util.StringTools; import org.deegree.framework.xml.DOMPrinter; import org.deegree.framework.xml.XMLException; import org.deegree.framework.xml.XMLFragment; import org.deegree.framework.xml.XMLTools; import org.deegree.model.spatialschema.Envelope; import org.deegree.model.spatialschema.GMLGeometryAdapter; import org.deegree.model.spatialschema.Geometry; import org.deegree.model.spatialschema.GeometryException; import org.deegree.ogcbase.CommonNamespaces; import org.w3c.dom.Element; import org.xml.sax.SAXException; /** * Exports feature instances to their GML representation. * <p> * Has support for XLink output and to disable XLink output (which is generally not feasible). * * TODO Handle FeatureCollections like ordinary Features (change model). * TODO Separate cycle check (for suppressXLinkOutput). * TODO Use a more straight-forward approach to export DOM representations. * * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider </a> * @author last edited by: $Author: mschneider $ * * @version $Revision: 1.73 $, $Date: 2006/11/09 17:45:53 $ */ public class GMLFeatureAdapter { private static final ILogger LOG = LoggerFactory.getLogger( GMLFeatureAdapter.class ); // values: feature ids of already exported features (for XLinks) private Set<String> exportedFeatures = new HashSet<String>(); // values: feature ids of all (sub-) features in a feature (to find cyclic features) private Set<String> localFeatures = new HashSet<String>(); private boolean suppressXLinkOutput; private String schemaURL; /** * Creates a new <code>GMLFeatureAdapter</code> instance with enabled XLink output. */ public GMLFeatureAdapter() { this.suppressXLinkOutput = false; } /** * Creates a new <code>GMLFeatureAdapter</code> instance with enabled XLink output and schema * reference. * * @param schemaURL * URL of schema document (used as xsi:schemaLocation attribute in XML output) */ public GMLFeatureAdapter( String schemaURL ) { this.suppressXLinkOutput = false; if ( schemaURL != null ) { this.schemaURL = StringTools.replace( schemaURL, "&", "&", true ); } } /** * Creates a new instance <code>GMLFeatureAdapter</code> with configurable XLink output. * * @param suppressXLinkOutput * set to true, if no XLinks shall be used */ public GMLFeatureAdapter( boolean suppressXLinkOutput ) { this.suppressXLinkOutput = suppressXLinkOutput; } /** * Creates a new instance <code>GMLFeatureAdapter</code> with configurable XLink output. * * @param suppressXLinkOutput * set to true, if no XLinks shall be used * @param schemaURL * URL of schema document (used as xsi:schemaLocation attribute in XML output) */ public GMLFeatureAdapter( boolean suppressXLinkOutput, String schemaURL ) { this.suppressXLinkOutput = suppressXLinkOutput; if ( schemaURL != null ) { this.schemaURL = StringTools.replace( schemaURL, "&", "&", true ); } } /** * Appends the DOM representation of the given feature to the also given <code>Node</code>. * <p> * TODO do this a better way (append nodes directly without serializing to string and * parsing it again) * * @param root * @param feature * @throws FeatureException * @throws IOException * @throws SAXException */ public void append( Element root, Feature feature ) throws FeatureException, IOException, SAXException { GMLFeatureDocument doc = export( feature ); XMLTools.insertNodeInto( doc.getRootElement(), root ); } /** * Export a <code>Feature</code> to it's XML representation. * * @param feature feature to export * @return XML representation of feature * @throws IOException * @throws FeatureException * @throws XMLException * @throws SAXException */ public GMLFeatureDocument export( Feature feature ) throws IOException, FeatureException, XMLException, SAXException { ByteArrayOutputStream bos = new ByteArrayOutputStream( 20000 ); export( feature, bos ); ByteArrayInputStream bis = new ByteArrayInputStream( bos.toByteArray() ); bos.close(); GMLFeatureDocument doc = new GMLFeatureDocument(); doc.load( bis, XMLFragment.DEFAULT_URL ); return doc; } /** * Appends the DOM representation of the given <code>FeatureCollection</code> to the * also given <code>Node</code>. * <p> * TODO do this a better way (append nodes directly without serializing to string and * parsing it again) * * @param root * @param fc * @throws FeatureException * @throws IOException * @throws SAXException */ public void append( Element root, FeatureCollection fc ) throws FeatureException, IOException, SAXException { GMLFeatureCollectionDocument doc = export( fc ); XMLTools.insertNodeInto( doc.getRootElement(), root ); } /** * Export a <code>FeatureCollection</code> to it's XML representation. * * @param fc feature collection * @return XML representation of feature collection * @throws IOException * @throws FeatureException * @throws XMLException * @throws SAXException */ public GMLFeatureCollectionDocument export( FeatureCollection fc ) throws IOException, FeatureException, XMLException, SAXException { ByteArrayOutputStream bos = new ByteArrayOutputStream( 20000 ); export( fc, bos ); ByteArrayInputStream bis = new ByteArrayInputStream( bos.toByteArray() ); bos.close(); GMLFeatureCollectionDocument doc = new GMLFeatureCollectionDocument(); doc.load( bis, XMLFragment.DEFAULT_URL ); return doc; } /** * Exports an instance of a <code>FeatureCollection</code> to the passed * <code>OutputStream</code> formatted as GML. Uses the deegree system character set for the * XML header encoding information. * * @param fc * feature collection to export * @param os * output stream to write to * * @throws IOException * @throws FeatureException */ public void export( FeatureCollection fc, OutputStream os ) throws IOException, FeatureException { export( fc, os, CharsetUtils.getSystemCharset() ); } /** * Exports a <code>FeatureCollection</code> instance to the passed <code>OutputStream</code> * formatted as GML. * * @param fc * feature collection to export * @param os * output stream to write to * @param charsetName * name of the used charset/encoding (for the XML header) * * @throws IOException * @throws FeatureException */ public void export( FeatureCollection fc, OutputStream os, String charsetName ) throws IOException, FeatureException { PrintWriter pw = new PrintWriter( new OutputStreamWriter( os, charsetName ) ); pw.println( "<?xml version=\"1.0\" encoding=\"" + charsetName + "\"?>" ); exportRootCollection( fc, pw ); pw.close(); } /** * Exports a <code>FeatureCollection</code> instance to the passed <code>OutputStream</code> * formatted as GML. * * @param fc * feature collection to print/export * @param pw * target of the printing/export * @throws FeatureException */ private void exportRootCollection( FeatureCollection fc, PrintWriter pw ) throws FeatureException { if ( fc.getId() != null && !"".equals( fc.getId() ) ) { this.exportedFeatures.add( fc.getId() ); } // open the feature collection element pw.print( "<" ); pw.print( fc.getName().getAsString() ); Map<String, String> attributes = fc.getAttributes(); for ( Iterator iterator = attributes.keySet().iterator(); iterator.hasNext(); ) { String name = (String) iterator.next(); String value = attributes.get( name ); pw.print( ' ' ); pw.print( name ); pw.print( "='" ); pw.print( value ); pw.print( "'" ); } // determine and add namespace bindings Map<String, URI> nsBindings = determineUsedNSBindings( fc ); nsBindings.put( "gml", CommonNamespaces.GMLNS ); nsBindings.put( "xlink", CommonNamespaces.XLNNS ); if ( this.schemaURL != null ) { nsBindings.put( "xsi", CommonNamespaces.XSINS ); } appendNSBindings( nsBindings, pw ); // add schema reference (if available) if ( this.schemaURL != null ) { pw.print( " xsi:schemaLocation=\"http://www.deegree.org/app " ); pw.print( this.schemaURL + "\"" ); } pw.print( '>' ); Envelope env; try { env = fc.getBoundedBy(); } catch ( GeometryException e ) { throw new FeatureException( "Error getting BBOX of feature collection: " + e.getMessage(), e ); } if ( env != null ) { pw.print( "<gml:boundedBy><gml:Envelope" ); if ( env.getCoordinateSystem() != null ) { pw.print( " srsName='" + env.getCoordinateSystem().getAsString() + "'" ); } pw.print( "><gml:pos srsDimension='2'>" ); pw.print( env.getMin().getX() ); pw.print( ' ' ); pw.print( env.getMin().getY() ); pw.print( "</gml:pos><gml:pos srsDimension='2'>" ); pw.print( env.getMax().getX() ); pw.print( ' ' ); pw.print( env.getMax().getY() ); pw.print( "</gml:pos></gml:Envelope></gml:boundedBy>" ); } // export all contained features for ( int i = 0; i < fc.size(); i++ ) { Feature feature = fc.getFeature( i ); String fid = feature.getId(); if ( fid != null && !fid.equals( "" ) && this.exportedFeatures.contains( fid ) && !this.suppressXLinkOutput ) { pw.print( "<gml:featureMember xlink:href=\"#" ); pw.print( fid ); pw.print( "\"/>" ); } else { pw.print( "<gml:featureMember>" ); export( feature, pw ); pw.print( "</gml:featureMember>" ); } } // close the feature collection element pw.print( "</" ); pw.print( fc.getName().getAsString() ); pw.print( '>' ); } /** * Determines the namespace bindings that are used in the feature collection. * <p> * NOTE: Currently only the bindings for the feature collection's root element and the contained * features are considered. If a subfeature uses another bindings, this binding will be missing * in the XML. * * @param fc * feature collection * @return the namespace bindings. */ private Map<String, URI> determineUsedNSBindings( FeatureCollection fc ) { Map<String, URI> nsBindings = new HashMap<String, URI>(); // process feature collection element QualifiedName name = fc.getName(); nsBindings.put( name.getPrefix(), name.getNamespace() ); // process contained features for ( int i = 0; i < fc.size(); i++ ) { name = fc.getFeature( i ).getName(); nsBindings.put( name.getPrefix(), name.getNamespace() ); } return nsBindings; } /** * Appends the given namespace bindings to the PrintWriter. * * @param bindings * namespace bindings to append * @param pw * PrintWriter to write to */ private void appendNSBindings( Map<String, URI> bindings, PrintWriter pw ) { Iterator<String> prefixIter = bindings.keySet().iterator(); while ( prefixIter.hasNext() ) { String prefix = prefixIter.next(); URI nsURI = bindings.get( prefix ); pw.print( " xmlns:" ); pw.print( prefix ); pw.print( "=\"" ); pw.print( nsURI ); pw.print( '\"' ); } } /** * Exports an instance of a <code>Feature</code> to the passed <code>OutputStream</code> * formatted as GML. Uses the deegree system character set for the XML header encoding * information. * * @param feature * feature to export * @param os * output stream to write to * * @throws IOException * @throws FeatureException */ public void export( Feature feature, OutputStream os ) throws IOException, FeatureException { export( feature, os, CharsetUtils.getSystemCharset() ); } /** * Exports a <code>Feature</code> instance to the passed <code>OutputStream</code> * formatted as GML. * * @param feature * feature to export * @param os * output stream to write to * @param charsetName * name of the used charset/encoding (for the XML header) * @throws IOException * @throws FeatureException */ public void export( Feature feature, OutputStream os, String charsetName ) throws IOException, FeatureException { PrintWriter pw = new PrintWriter( new OutputStreamWriter( os, charsetName ) ); pw.println( "<?xml version=\"1.0\" encoding=\"" + charsetName + "\"?>" ); export( feature, pw ); pw.close(); } /** * Exports a <code>Feature</code> instance to the passed <code>PrintWriter</code> as GML. * * @param feature * feature to export * @param pw * PrintWriter to write to * @throws FeatureException */ private void export( Feature feature, PrintWriter pw ) throws FeatureException { QualifiedName ftName = feature.getName(); String fid = feature.getId(); if ( this.suppressXLinkOutput && fid != null && !"".equals( fid ) ) { if ( this.localFeatures.contains( fid ) ) { String msg = Messages.format( "ERROR_CYLIC_FEATURE", fid ); throw new FeatureException( msg ); } this.localFeatures.add( fid ); } // open feature element (add gml:id attribute if feature has an id) pw.print( '<' ); pw.print( ftName.getAsString() ); if ( fid != null ) { this.exportedFeatures.add( fid ); pw.print( " gml:id=\"" ); pw.print( fid ); pw.print( '\"' ); } pw.print( '>' ); try { Envelope env = null; if ( ( env = feature.getBoundedBy() ) != null ) { pw.print( "<gml:boundedBy><gml:Envelope" ); if ( env.getCoordinateSystem() != null ) { pw.print( " srsName='" + env.getCoordinateSystem().getAsString() + "'" ); } pw.print( "><gml:pos srsDimension='2'>" ); pw.print( env.getMin().getX() ); pw.print( ' ' ); pw.print( env.getMin().getY() ); pw.print( "</gml:pos><gml:pos srsDimension=\"2\">" ); pw.print( env.getMax().getX() ); pw.print( ' ' ); pw.print( env.getMax().getY() ); pw.print( "</gml:pos></gml:Envelope></gml:boundedBy>" ); } } catch ( GeometryException e ) { LOG.logError( e.getMessage(), e ); } // export all properties of the feature FeatureProperty[] properties = feature.getProperties(); for ( int i = 0; i < properties.length; i++ ) { if ( properties[i] != null && properties[i].getValue() != null ) { exportProperty( properties[i], pw ); } } // close feature element pw.print( "</" ); pw.print( ftName.getAsString() ); pw.println( '>' ); if ( this.suppressXLinkOutput || fid != null ) { this.localFeatures.remove( fid ); } } /** * Exports a <code>FeatureProperty</code> instance to the passed <code>PrintWriter</code> as * GML. * * @param property * property to export * @param pw * PrintWriter to write to * @throws FeatureException */ private void exportProperty( FeatureProperty property, PrintWriter pw ) throws FeatureException { QualifiedName propertyName = property.getName(); if ( property instanceof XLinkedFeatureProperty && !this.suppressXLinkOutput ) { pw.print( '<' ); pw.print( propertyName.getAsString() ); pw.print( " xlink:href=\"#" ); pw.print( ( (Feature) property.getValue() ).getId() ); pw.print( "\"/>" ); } else { pw.print( '<' ); pw.print( propertyName.getAsString() ); pw.print( '>' ); Object value = property.getValue(); if ( value != null ) { exportPropertyValue( value, pw ); } pw.print( "</" ); pw.print( propertyName.getAsString() ); pw.print( '>' ); } } /** * Exports the value of a property to the passed <code>PrintWriter</code> as GML. * * TODO Handle date * * @param value * property value to export * @param pw * PrintWriter to write to * @throws FeatureException */ private void exportPropertyValue( Object value, PrintWriter pw ) throws FeatureException { if ( value instanceof Feature ) { export( (Feature) value, pw ); } else if ( value instanceof Feature[] ) { Feature[] features = (Feature[]) value; for ( int i = 0; i < features.length; i++ ) { export( features[i], pw ); } } else if ( value instanceof Envelope ) { exportEnvelope( (Envelope) value, pw ); } else if ( value instanceof FeatureCollection ) { export( (FeatureCollection) value, pw ); } else if ( value instanceof Geometry ) { exportGeometry( (Geometry) value, pw ); } else if ( value instanceof Date ) { pw.print( ( (Date) value ).toString() ); // pw.print( TimeTools.getISOFormattedTime( (Date) value ) ); } else if ( value instanceof Calendar ) { pw.print( ( (Calendar) value ).getTime().toString() ); } else if ( value instanceof Integer || value instanceof Long || value instanceof Float || value instanceof Double || value instanceof BigDecimal ) { pw.print( value.toString() ); } else if ( value instanceof String ) { StringBuffer sb = DOMPrinter.validateCDATA( (String) value ); pw.print( sb ); } else if ( value instanceof Boolean ) { pw.print( value ); } else { LOG.logInfo( "Unhandled property class '" + value.getClass() + "' in GMLFeatureAdapter." ); StringBuffer sb = DOMPrinter.validateCDATA( value.toString() ); pw.print( sb ); } } /** * prints the passed geometry to the also passed PrintWriter formatted as GML * * @param geo * geometry to print/extport * @param pw * target of the printing/export * @throws FeatureException */ private void exportGeometry( Geometry geo, PrintWriter pw ) throws FeatureException { LOG.entering(); try { pw.print( GMLGeometryAdapter.export( geo ) ); } catch ( Exception e ) { LOG.logError( "", e ); throw new FeatureException( "Could not export geometry to GML: " + e.getMessage(), e ); } LOG.exiting(); } /** * prints the passed geometry to the also passed PrintWriter formatted as GML * * @param geo * geometry to print/extport * @param pw * target of the printing/export * @throws FeatureException */ private void exportEnvelope( Envelope geo, PrintWriter pw ) throws FeatureException { LOG.entering(); try { pw.print( GMLGeometryAdapter.exportAsBox( geo ) ); } catch ( Exception e ) { throw new FeatureException( "Could not export envelope to GML: " + e.getMessage(), e ); } LOG.exiting(); } } /* ******************************************************************** Changes to this class. What the people have been up to: $Log: GMLFeatureAdapter.java,v $ Revision 1.73 2006/11/09 17:45:53 mschneider Added srsName attribute in Envelopes. Revision 1.72 2006/10/17 20:31:19 poth *** empty log message *** Revision 1.71 2006/10/17 15:21:57 mschneider Bug fix: use of XLinked root feature members was not possible. Revision 1.70 2006/10/11 20:31:45 mschneider Added encoding of & (to &) in schemaURL, because it is used in xsi:schemaLocation attribute. Revision 1.69 2006/10/11 18:00:01 mschneider Added constructors that allow to place a schema reference in generated feature collections. Revision 1.68 2006/08/23 16:37:35 mschneider Fixed output of " in 'gml:Envelope' elements. Revision 1.67 2006/08/11 09:29:40 poth strings replace by chars where possible / add srsDimension for global Envelope Revision 1.66 2006/08/01 10:41:10 mschneider Added checks for empty fids (""). Revision 1.65 2006/07/25 15:52:17 mschneider Improved javadoc. Revision 1.64 2006/06/21 10:24:13 mschneider Added methods to export to DOM. Revision 1.63 2006/06/15 18:30:48 poth *** empty log message *** Revision 1.62 2006/05/09 15:51:37 poth *** empty log message *** Revision 1.61 2006/04/27 09:44:59 poth *** empty log message *** Revision 1.60 2006/04/26 13:34:09 poth *** empty log message *** Revision 1.59 2006/04/24 09:34:31 poth *** empty log message *** Revision 1.58 2006/04/18 18:22:55 poth *** empty log message *** Revision 1.26 2006/04/18 18:20:46 poth *** empty log message *** Revision 1.25 2006/04/18 14:40:32 mschneider Removed unnecessary whitespace on attribute output. Revision 1.24 2006/04/11 15:12:31 poth *** empty log message *** Revision 1.23 2006/04/07 17:14:34 mschneider Added workaround for XLinkedFeatureProperties. Revision 1.22 2006/04/06 20:25:27 poth *** empty log message *** Revision 1.21 2006/04/04 20:39:42 poth *** empty log message *** Revision 1.20 2006/03/30 21:20:26 poth *** empty log message *** Revision 1.19 2006/03/18 22:47:19 mschneider Added handling of Long properties to exportPropertyValue. Revision 1.18 2006/02/26 21:30:42 poth *** empty log message *** Revision 1.17 2006/02/21 23:57:15 mschneider Removed output of empty (null valued) features. Revision 1.16 2006/02/09 10:36:03 mschneider Improved javadoc. Revision 1.15 2006/02/09 09:53:44 mschneider Implemented suppressXLinkOutput. Revision 1.14 2006/02/08 17:43:17 mschneider Major cleanup. Revision 1.13 2006/02/05 21:21:13 mschneider Began implementation of option to suppress xlink output. Revision 1.12 2006/02/05 00:18:29 mschneider Initial version. Necessary for XLink handling. ********************************************************************** */