001    //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.3_testing/src/org/deegree/io/datastore/schema/MappedGMLSchemaDocument.java $
002    /*----------------------------------------------------------------------------
003     This file is part of deegree, http://deegree.org/
004     Copyright (C) 2001-2009 by:
005       Department of Geography, University of Bonn
006     and
007       lat/lon GmbH
008    
009     This library is free software; you can redistribute it and/or modify it under
010     the terms of the GNU Lesser General Public License as published by the Free
011     Software Foundation; either version 2.1 of the License, or (at your option)
012     any later version.
013     This library is distributed in the hope that it will be useful, but WITHOUT
014     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
015     FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
016     details.
017     You should have received a copy of the GNU Lesser General Public License
018     along with this library; if not, write to the Free Software Foundation, Inc.,
019     59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
020    
021     Contact information:
022    
023     lat/lon GmbH
024     Aennchenstr. 19, 53177 Bonn
025     Germany
026     http://lat-lon.de/
027    
028     Department of Geography, University of Bonn
029     Prof. Dr. Klaus Greve
030     Postfach 1147, 53001 Bonn
031     Germany
032     http://www.geographie.uni-bonn.de/deegree/
033    
034     e-mail: info@deegree.org
035    ----------------------------------------------------------------------------*/
036    package org.deegree.io.datastore.schema;
037    
038    import static org.deegree.framework.xml.XMLTools.getElements;
039    import static org.deegree.framework.xml.XMLTools.getNodeAsBoolean;
040    import static org.deegree.framework.xml.XMLTools.getNodeAsQualifiedName;
041    import static org.deegree.framework.xml.XMLTools.getRequiredElement;
042    import static org.deegree.framework.xml.XMLTools.getRequiredNodeAsQualifiedName;
043    import static org.deegree.ogcbase.CommonNamespaces.DEEGREEWFS_PREFIX;
044    import static org.deegree.ogcbase.CommonNamespaces.XS_PREFIX;
045    
046    import java.net.URI;
047    import java.util.ArrayList;
048    import java.util.Iterator;
049    import java.util.LinkedList;
050    import java.util.List;
051    import java.util.Properties;
052    
053    import org.deegree.datatypes.QualifiedName;
054    import org.deegree.datatypes.Types;
055    import org.deegree.datatypes.UnknownTypeException;
056    import org.deegree.framework.log.ILogger;
057    import org.deegree.framework.log.LoggerFactory;
058    import org.deegree.framework.util.Pair;
059    import org.deegree.framework.xml.XMLParsingException;
060    import org.deegree.framework.xml.XMLTools;
061    import org.deegree.framework.xml.schema.ComplexTypeDeclaration;
062    import org.deegree.framework.xml.schema.ElementDeclaration;
063    import org.deegree.framework.xml.schema.SimpleTypeDeclaration;
064    import org.deegree.framework.xml.schema.TypeDeclaration;
065    import org.deegree.framework.xml.schema.TypeReference;
066    import org.deegree.framework.xml.schema.XMLSchemaException;
067    import org.deegree.i18n.Messages;
068    import org.deegree.io.datastore.AnnotationDocument;
069    import org.deegree.io.datastore.Datastore;
070    import org.deegree.io.datastore.DatastoreConfiguration;
071    import org.deegree.io.datastore.DatastoreRegistry;
072    import org.deegree.io.datastore.idgenerator.IdGenerator;
073    import org.deegree.io.datastore.schema.MappedGMLId.IDPART_INFO;
074    import org.deegree.io.datastore.schema.TableRelation.FK_INFO;
075    import org.deegree.io.datastore.schema.content.ConstantContent;
076    import org.deegree.io.datastore.schema.content.FieldContent;
077    import org.deegree.io.datastore.schema.content.FunctionParam;
078    import org.deegree.io.datastore.schema.content.MappingField;
079    import org.deegree.io.datastore.schema.content.MappingGeometryField;
080    import org.deegree.io.datastore.schema.content.SQLFunctionCall;
081    import org.deegree.io.datastore.schema.content.SimpleContent;
082    import org.deegree.io.datastore.schema.content.SpecialContent;
083    import org.deegree.model.crs.UnknownCRSException;
084    import org.deegree.model.feature.schema.GMLSchemaDocument;
085    import org.deegree.model.feature.schema.PropertyType;
086    import org.deegree.ogcbase.CommonNamespaces;
087    import org.w3c.dom.Element;
088    import org.w3c.dom.Node;
089    
090    /**
091     * Parser for GML schema documents which are annotated with mapping (persistence) information.
092     *
093     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider </a>
094     * @author last edited by: $Author: mschneider $
095     *
096     * @version $Revision: 18195 $, $Date: 2009-06-18 17:55:39 +0200 (Do, 18. Jun 2009) $
097     */
098    public class MappedGMLSchemaDocument extends GMLSchemaDocument {
099    
100        private static final long serialVersionUID = 8293629056821438839L;
101    
102        private static final ILogger LOG = LoggerFactory.getLogger( MappedGMLSchemaDocument.class );
103    
104        private String namespacePrefix;
105    
106        private URI defaultSRS;
107    
108        private boolean suppressXLinkOutput;
109    
110        private DatastoreConfiguration dsConfiguration;
111    
112        /**
113         * Returns the class representation of the underlying mapped GML schema document.
114         *
115         * @return the class representation of the underlying mapped GML schema document
116         * @throws XMLParsingException
117         * @throws XMLSchemaException
118         * @throws UnknownCRSException
119         */
120        public MappedGMLSchema parseMappedGMLSchema()
121                                throws XMLParsingException, XMLSchemaException, UnknownCRSException {
122            parseGlobalAnnotations();
123            SimpleTypeDeclaration[] simpleTypes = extractSimpleTypeDeclarations();
124            ComplexTypeDeclaration[] complexTypes = extractComplexTypeDeclarations();
125            ElementDeclaration[] elementDeclarations = extractElementDeclarations();
126            return new MappedGMLSchema( getTargetNamespace(), simpleTypes, complexTypes, elementDeclarations,
127                                        this.namespacePrefix, this.defaultSRS, this.dsConfiguration,
128                                        this.suppressXLinkOutput, this );
129        }
130    
131        /**
132         * Parses the global "xs:annotation/xs:appinfo" block.
133         * <p>
134         * Delegates the datastore specific configuration options to the responsible {@link AnnotationDocument} parser.
135         *
136         * @throws XMLParsingException
137         */
138        @SuppressWarnings("unchecked")
139        private void parseGlobalAnnotations()
140                                throws XMLParsingException {
141    
142            Element appinfoElement = (Element) XMLTools.getRequiredNode( getRootElement(), "xs:annotation/xs:appinfo",
143                                                                         nsContext );
144            this.namespacePrefix = XMLTools.getRequiredNodeAsString( appinfoElement, "deegreewfs:Prefix", nsContext );
145            String backend = XMLTools.getRequiredNodeAsString( appinfoElement, "deegreewfs:Backend", nsContext );
146            this.suppressXLinkOutput = XMLTools.getNodeAsBoolean( appinfoElement, "deegreewfs:SuppressXLinkOutput/text()",
147                                                                  nsContext, false );
148            this.defaultSRS = XMLTools.getRequiredNodeAsURI( appinfoElement, "deegreewfs:DefaultSRS", nsContext );
149    
150            Class<Datastore> datastoreClass = null;
151            try {
152                datastoreClass = DatastoreRegistry.getDatastoreClass( backend );
153            } catch ( IllegalArgumentException e ) {
154                String msg = Messages.getMessage( "DATASTORE_UNKNOWN_TYPE_CODE", backend );
155                LOG.logInfo( msg );
156                try {
157                    datastoreClass = (Class<Datastore>) Class.forName( backend );
158                } catch ( ClassNotFoundException e1 ) {
159                    msg = Messages.getMessage( "DATASTORE_UNKNOWN_TYPE_AND_CLASS", backend );
160                    throw new XMLParsingException( msg );
161                }
162            }
163    
164            AnnotationDocument annotationParser = null;
165            try {
166                Datastore ds = datastoreClass.newInstance();
167                annotationParser = ds.getAnnotationParser();
168            } catch ( Exception e ) {
169                String msg = Messages.getMessage( "DATASTORE_CLASS_INSTANTIATION_ERROR", datastoreClass.getName(),
170                                                  e.getMessage() );
171                throw new XMLParsingException( msg );
172            }
173    
174            annotationParser.setRootElement( this.getRootElement() );
175            annotationParser.setSystemId( this.getSystemId() );
176            try {
177                this.dsConfiguration = annotationParser.parseDatastoreConfiguration();
178            } catch ( XMLParsingException e ) {
179                LOG.logError( e.getMessage(), e );
180                String msg = Messages.getMessage( "DATASTORE_CONFIGURATION_BLOCK_FAULTY", e.getMessage() );
181                throw new XMLParsingException( msg );
182            }
183        }
184    
185        /**
186         * Parses the given <code>Element</code> as an annotated 'xs:element' declaration (with mapping information).
187         *
188         * @param element
189         *            'xs:element' declaration to be parsed
190         * @return object representation of the declaration
191         * @throws XMLParsingException
192         *             if the document is not a valid XML Schema document or does not match the limitations of this class
193         */
194        @Override
195        protected MappedElementDeclaration parseElementDeclaration( Element element )
196                                throws XMLParsingException {
197    
198            QualifiedName name = new QualifiedName( XMLTools.getRequiredNodeAsString( element, "@name", nsContext ),
199                                                    getTargetNamespace() );
200            if ( name.getLocalName().length() == 0 ) {
201                String msg = "Error in schema document. Empty name (\"\") in element declaration found.";
202                throw new XMLSchemaException( msg );
203            }
204    
205            boolean isAbstract = XMLTools.getNodeAsBoolean( element, "@abstract", nsContext, false );
206    
207            TypeReference typeReference = null;
208            Node typeNode = XMLTools.getNode( element, "@type", nsContext );
209            if ( typeNode != null ) {
210                typeReference = new TypeReference( parseQualifiedName( typeNode ) );
211            } else {
212                if ( LOG.isDebug() ) {
213                    LOG.logDebug( "Trying to find a complexType below the element named " + element.getAttribute( "name" )
214                                  + "." );
215                }
216                // inline type declaration
217                Element elem = (Element) XMLTools.getRequiredNode( element, getFullName( "complexType" ), nsContext );
218                TypeDeclaration type = parseComplexTypeDeclaration( elem );
219                typeReference = new TypeReference( type );
220            }
221    
222            int minOccurs = XMLTools.getNodeAsInt( element, "@minOccurs", nsContext, 1 );
223            int maxOccurs = -1;
224            String maxOccursString = XMLTools.getNodeAsString( element, "@maxOccurs", nsContext, "1" );
225            if ( !"unbounded".equals( maxOccursString ) ) {
226                try {
227                    maxOccurs = Integer.parseInt( maxOccursString );
228                } catch ( NumberFormatException e ) {
229                    throw new XMLParsingException( "Invalid value ('" + maxOccursString + "') in 'maxOccurs' attribute. "
230                                                   + "Must be a valid integer value or 'unbounded'." );
231                }
232            }
233    
234            QualifiedName substitutionGroup = null;
235            Node substitutionGroupNode = XMLTools.getNode( element, "@substitutionGroup", nsContext );
236            if ( substitutionGroupNode != null ) {
237                substitutionGroup = parseQualifiedName( substitutionGroupNode );
238            }
239    
240            Element annotationElement = (Element) XMLTools.getNode( element, getFullName( "annotation" ), nsContext );
241    
242            return new MappedElementDeclaration( name, isAbstract, typeReference, minOccurs, maxOccurs, substitutionGroup,
243                                                 annotationElement );
244        }
245    
246        /**
247         * Parses the given <code>Element</code> as an annotated 'xs:complexType' declaration (with mapping information).
248         *
249         * @param element
250         *            'xs:complexType' declaration to be parsed
251         * @return object representation of the declaration
252         * @throws XMLParsingException
253         *             if the document is not a valid XML Schema document or does not match the limitations of this class
254         */
255        @Override
256        protected MappedComplexTypeDeclaration parseComplexTypeDeclaration( Element element )
257                                throws XMLParsingException {
258    
259            QualifiedName name = null;
260            String localName = XMLTools.getNodeAsString( element, "@name", nsContext, null );
261            if ( localName != null ) {
262                name = new QualifiedName( localName, getTargetNamespace() );
263                if ( localName.length() == 0 ) {
264                    String msg = "Error in schema document. Empty name (\"\") for complexType " + "declaration found.";
265                    throw new XMLSchemaException( msg );
266                }
267            }
268    
269            List<Node> subElementList = null;
270            TypeReference extensionBase = null;
271            Node extensionBaseNode = XMLTools.getNode( element, getFullName( "complexContent/" )
272                                                                + getFullName( "extension/@base" ), nsContext );
273            if ( extensionBaseNode != null ) {
274                extensionBase = new TypeReference( parseQualifiedName( extensionBaseNode ) );
275                subElementList = XMLTools.getNodes( element, getFullName( "complexContent/" ) + getFullName( "extension/" )
276                                                             + getFullName( "sequence/" ) + getFullName( "element" ),
277                                                    nsContext );
278            } else {
279                subElementList = XMLTools.getRequiredNodes( element, getFullName( "sequence/" ) + getFullName( "element" ),
280                                                            nsContext );
281            }
282    
283            ElementDeclaration[] subElements = new ElementDeclaration[subElementList.size()];
284            for ( int i = 0; i < subElements.length; i++ ) {
285                Element subElement = (Element) subElementList.get( i );
286                subElements[i] = parseElementDeclaration( subElement );
287            }
288            Element annotationElement = (Element) XMLTools.getNode( element, getFullName( "annotation" ), nsContext );
289    
290            return new MappedComplexTypeDeclaration( name, extensionBase, subElements, annotationElement );
291        }
292    
293        /**
294         * Extracts the "gml:id" information from the given "xs:annotation" element.
295         *
296         * @param annotationElement
297         *            "xs:annotation" element
298         * @param defaultTableName
299         *            name for table if "deegreewfs:table"-element is missing
300         * @return "gml:id" information as MappedGMLId
301         * @throws XMLSchemaException
302         *             if a syntactic or semantic error is found
303         */
304        MappedGMLId extractGMLId( Element annotationElement, String defaultTableName )
305                                throws XMLSchemaException {
306            MappedGMLId gmlId = null;
307            try {
308                String table = XMLTools.getNodeAsString( annotationElement, "xs:appinfo/deegreewfs:table/text()",
309                                                         nsContext, defaultTableName );
310                Element gmlIdElement = (Element) XMLTools.getNode( annotationElement, "xs:appinfo/deegreewfs:gmlId",
311                                                                   nsContext );
312                if ( gmlIdElement != null ) {
313                    gmlId = parseGMLIdDefinition( gmlIdElement, table );
314                } else {
315                    MappingField[] idFields = new MappingField[] { new MappingField( table, "fid", Types.VARCHAR ) };
316                    IdGenerator idGenerator = IdGenerator.getInstance( IdGenerator.TYPE_UUID, new Properties() );
317                    gmlId = new MappedGMLId( defaultTableName.toUpperCase(), "_", idFields, idGenerator,
318                                             IDPART_INFO.notIDPart );
319                }
320            } catch ( XMLParsingException e ) {
321                throw new XMLSchemaException( e.getMessage(), e );
322            }
323            return gmlId;
324        }
325    
326        /**
327         * Extracts the mapping information for a simple property type from the given "xs:annotation" element.
328         *
329         * @param element
330         *            "xs:annotation" element
331         * @param propertyName
332         *            name of the property (local part is used as default field name if it is not specified explicitly in
333         *            the "MappingField" element)
334         * @param type
335         * @param minOccurs
336         * @param maxOccurs
337         * @param isIdentityPart
338         * @param table
339         *            name of the table that is associated with the feature type that this property belongs to
340         * @return simple property type with persistence information
341         * @throws XMLSchemaException
342         *             if a syntactic or semantic error is found
343         */
344        MappedSimplePropertyType parseMappedSimplePropertyType( Element element, QualifiedName propertyName, int type,
345                                                                int minOccurs, int maxOccurs, boolean isIdentityPart,
346                                                                String table )
347                                throws XMLSchemaException {
348    
349            MappedSimplePropertyType pt = null;
350            try {
351                Element contentElement = (Element) XMLTools.getRequiredNode( element, "xs:appinfo/deegreewfs:Content",
352                                                                             nsContext );
353                // sanity check (common user error)
354                Node typeNode = XMLTools.getNode( contentElement, "@type", nsContext );
355                if ( typeNode != null ) {
356                    String msg = "Content element for simple property type '" + propertyName
357                                 + "' must not contain a type attribute.";
358                    throw new XMLParsingException( msg );
359                }
360    
361                // table relations
362                List<Node> relationElements = XMLTools.getNodes( contentElement, "deegreewfs:Relation", nsContext );
363                TableRelation[] tableRelations = new TableRelation[relationElements.size()];
364                String fromTable = table;
365                for ( int i = 0; i < tableRelations.length; i++ ) {
366                    tableRelations[i] = parseTableRelation( (Element) relationElements.get( i ), fromTable );
367                    fromTable = tableRelations[i].getToTable();
368                }
369    
370                SimpleContent content = null;
371    
372                // check type of content
373                Node node = XMLTools.getNode( contentElement, "deegreewfs:MappingField", nsContext );
374                if ( node != null ) {
375                    String defaultField = propertyName.getLocalName();
376                    content = parseMappingField( (Element) node, defaultField, table );
377                } else {
378                    node = XMLTools.getNode( contentElement, "deegreewfs:Constant", nsContext );
379                    if ( node != null ) {
380                        content = parseConstantContent( (Element) node );
381                    } else {
382                        node = XMLTools.getNode( contentElement, "deegreewfs:SQLFunctionCall", nsContext );
383                        if ( node != null ) {
384                            content = parseSQLFunctionCall( (Element) node, table );
385                        } else {
386                            String msg = Messages.getMessage( "DATASTORE_ANNOTATION_SIMPLE_CONTENT_ERROR", propertyName );
387                            throw new XMLParsingException( msg );
388                        }
389                    }
390                }
391    
392                pt = new MappedSimplePropertyType( propertyName, type, minOccurs, maxOccurs, isIdentityPart,
393                                                   tableRelations, content );
394            } catch ( XMLParsingException e ) {
395                throw new XMLSchemaException( "Error in definition of simple property '" + propertyName + "': "
396                                              + e.getMessage() );
397            }
398            return pt;
399        }
400    
401        /**
402         * Parses the given element as a "deegreewfs:Constant" element.
403         *
404         * @param element
405         *            "deegreewfs:Constant" element
406         * @return java representation of element
407         * @throws XMLParsingException
408         */
409        ConstantContent parseConstantContent( Element element )
410                                throws XMLParsingException {
411            String constant = XMLTools.getRequiredNodeAsString( element, "text()", nsContext );
412            return new ConstantContent( constant );
413        }
414    
415        /**
416         * Parses the given "deegreewfs:SpecialContent" element.
417         *
418         * @param element
419         *            "deegreewfs:SpecialContent" element
420         * @return java representation of element
421         * @throws XMLParsingException
422         */
423        SpecialContent parseSpecialContent( Element element )
424                                throws XMLParsingException {
425    
426            SpecialContent content = null;
427            String variable = XMLTools.getRequiredNodeAsString( element, "text()", nsContext );
428    
429            try {
430                content = new SpecialContent( variable );
431            } catch ( Exception e ) {
432                String msg = Messages.getMessage( "DATASTORE_PARSING_SPECIAL_CONTENT", e.getMessage() );
433                throw new XMLParsingException( msg );
434            }
435            return content;
436        }
437    
438        /**
439         * Parses the given element as a "deegreewfs:SQLFunctionCall" element.
440         *
441         * @param element
442         *            "deegreewfs:SQLFunctionCall" element
443         * @param table
444         * @return java representation of element
445         * @throws XMLParsingException
446         */
447        SQLFunctionCall parseSQLFunctionCall( Element element, String table )
448                                throws XMLParsingException {
449    
450            String callString = XMLTools.getRequiredNodeAsString( element, "@call", nsContext );
451    
452            String typeName = XMLTools.getRequiredNodeAsString( element, "@type", nsContext );
453            int typeCode = -1;
454            try {
455                typeCode = Types.getTypeCodeForSQLType( typeName );
456            } catch ( UnknownTypeException e ) {
457                throw new XMLParsingException( "Invalid field type: " + e.getMessage(), e );
458            }
459    
460            List<Node> nl = XMLTools.getNodes( element, "deegreewfs:FunctionParam", nsContext );
461            List<FunctionParam> functionParams = new ArrayList<FunctionParam>();
462            Iterator<Node> iter = nl.iterator();
463            while ( iter.hasNext() ) {
464                Element paramElement = (Element) iter.next();
465                functionParams.add( parseFunctionParam( paramElement, table ) );
466            }
467    
468            // validate variable references
469            int maxVar = extractMaxVariableNumber( callString );
470            if ( maxVar > functionParams.size() ) {
471                String msg = "Error in FunctionCall definition ('" + callString + "') - call string uses variable $"
472                             + maxVar + ", but only has " + functionParams.size() + " FunctionParam elements.";
473                throw new XMLParsingException( msg );
474            }
475    
476            // check SRS for all function params (mapped geometry columns)
477            int internalSRS = -1;
478            for ( FunctionParam param : functionParams ) {
479                if ( param instanceof FieldContent ) {
480                    MappingField mf = ( (FieldContent) param ).getField();
481                    if ( mf instanceof MappingGeometryField ) {
482                        int thisSRS = ( (MappingGeometryField) mf ).getSRS();
483                        if ( internalSRS == -1 ) {
484                            internalSRS = thisSRS;
485                        } else {
486                            if ( internalSRS != thisSRS ) {
487                                String msg = Messages.getMessage( "DATASTORE_SQL_FUNCTION_CALL_INVALID_SRS", internalSRS,
488                                                                  thisSRS );
489                                throw new XMLParsingException( msg );
490                            }
491                        }
492                    }
493                }
494            }
495    
496            // set SRS for all 'SpecialContent' function params
497            for ( FunctionParam param : functionParams ) {
498                if ( param instanceof SpecialContent ) {
499                    ( (SpecialContent) param ).setSRS( internalSRS );
500                }
501            }
502    
503            return new SQLFunctionCall( callString, typeCode, functionParams );
504        }
505    
506        /**
507         * Extracts maximum variable numbers ('$i') used in the given call string.
508         *
509         * TODO: handle leading zeros
510         *
511         * @param callString
512         * @return maximum variable numbers used in the given call string
513         */
514        private int extractMaxVariableNumber( String callString )
515                                throws XMLParsingException {
516    
517            int maxVar = 0;
518    
519            int foundAt = callString.indexOf( '$' );
520            while ( foundAt != -1 ) {
521                foundAt++;
522                String varNumberString = "";
523                while ( foundAt < callString.length() ) {
524                    char numberChar = callString.charAt( foundAt++ );
525                    if ( numberChar >= '0' && numberChar <= '9' ) {
526                        varNumberString += numberChar;
527                    } else {
528                        break;
529                    }
530                }
531    
532                if ( varNumberString.length() == 0 ) {
533                    String msg = "Error in call attribute ('" + callString
534                                 + "') of FunctionCall definition - parameters must be specified by "
535                                 + "the '$' symbol, followed by a positive integer.";
536                    throw new XMLParsingException( msg );
537                }
538    
539                try {
540                    int varNo = Integer.parseInt( varNumberString );
541                    if ( varNo > maxVar ) {
542                        maxVar = varNo;
543                    }
544                    if ( varNo == 0 ) {
545                        String msg = "Error in call attribute ('" + callString
546                                     + "') of FunctionCall definition - $0 is not a valid variable "
547                                     + "identifier. Counting of variables starts with 1.";
548                        throw new XMLParsingException( msg );
549                    }
550                } catch ( NumberFormatException e ) {
551                    assert ( false );
552                }
553    
554                // find next '$' symbol
555                foundAt = callString.indexOf( '$', foundAt );
556            }
557            return maxVar;
558        }
559    
560        /**
561         * Parses the given "deegreewfs:FunctionParam" element.
562         * <p>
563         * Valid child elements:
564         * <ul>
565         * <li>deegreewfs:MappingField</li>
566         * <li>deegreewfs:ConstantContent</li>
567         * <li>deegreewfs:SpecialContent</li>
568         * </ul>
569         *
570         * @param element
571         *            "deegreewfs:FunctionParam" element
572         * @param table
573         * @return java representation of element
574         * @throws XMLParsingException
575         */
576        FunctionParam parseFunctionParam( Element element, String table )
577                                throws XMLParsingException {
578    
579            FunctionParam param = null;
580    
581            Element childElement = XMLTools.getFirstChildElement( element );
582            if ( childElement == null || !childElement.getNamespaceURI().equals( CommonNamespaces.DEEGREEWFS.toString() ) ) {
583                String msg = Messages.getMessage( "DATASTORE_PARSING_SQL_FUNCTION_CALL" );
584                throw new XMLParsingException( msg );
585            }
586    
587            if ( "MappingField".equals( childElement.getLocalName() ) ) {
588                // table relations
589                List<Node> relationElements = XMLTools.getNodes( element, "deegreewfs:Relation", nsContext );
590                TableRelation[] tablePath = new TableRelation[relationElements.size()];
591                String fromTable = table;
592                for ( int i = 0; i < tablePath.length; i++ ) {
593                    tablePath[i] = parseTableRelation( (Element) relationElements.get( i ), fromTable );
594                    fromTable = tablePath[i].getToTable();
595                }
596                // TODO do this a better way
597                String type = XMLTools.getRequiredNodeAsString( childElement, "@type", nsContext );
598                MappingField field;
599                if ( "GEOMETRY".equals( type ) ) {
600                    field = parseGeometryMappingField( childElement, null, fromTable );
601                } else {
602                    field = parseMappingField( childElement, null, fromTable );
603                }
604                param = new FieldContent( field, tablePath );
605            } else if ( "ConstantContent".equals( childElement.getLocalName() ) ) {
606                param = parseConstantContent( childElement );
607            } else if ( "SpecialContent".equals( childElement.getLocalName() ) ) {
608                param = parseSpecialContent( childElement );
609            } else {
610                String msg = Messages.getMessage( "DATASTORE_PARSING_SQL_FUNCTION_CALL" );
611                throw new XMLParsingException( msg );
612            }
613    
614            return param;
615        }
616    
617        /**
618         * Extracts the mapping information for a geometry property type from the given "xs:annotation" element.
619         *
620         * @param element
621         *            "xs:annotation" element
622         * @param propertyName
623         *            name of the property (local part is used as default field name if it is not specified explicitly in
624         *            the "MappingField" element)
625         * @param typeName
626         * @param type
627         * @param minOccurs
628         * @param maxOccurs
629         * @param isIdentityPart
630         * @param table
631         *            name of the table that is associated with the feature type that this property belongs to
632         * @return geometry property type with persistence information
633         * @throws XMLSchemaException
634         *             if a syntactic or semantic error is found
635         * @throws UnknownCRSException
636         */
637        MappedGeometryPropertyType parseMappedGeometryPropertyType( Element element, QualifiedName propertyName,
638                                                                    QualifiedName typeName, int type, int minOccurs,
639                                                                    int maxOccurs, boolean isIdentityPart, String table )
640                                throws XMLSchemaException, UnknownCRSException {
641    
642            MappedGeometryPropertyType pt = null;
643            try {
644                Element contentElement = (Element) XMLTools.getRequiredNode( element, "xs:appinfo/deegreewfs:Content",
645                                                                             nsContext );
646                // sanity check (common error)
647                Node typeNode = XMLTools.getNode( contentElement, "@type", nsContext );
648                if ( typeNode != null ) {
649                    throw new XMLParsingException( "Content element must not contain a type attribute." );
650                }
651    
652                URI srs = XMLTools.getNodeAsURI( element, "deegreewfs:SRS/text()", nsContext, this.defaultSRS );
653    
654                Element mfElement = (Element) XMLTools.getRequiredNode( contentElement, "deegreewfs:MappingField",
655                                                                        nsContext );
656                String defaultField = propertyName.getLocalName();
657                MappingGeometryField mappingField = parseGeometryMappingField( mfElement, defaultField, table );
658                List<Node> relationElements = XMLTools.getNodes( contentElement, "deegreewfs:Relation", nsContext );
659                TableRelation[] tableRelations = new TableRelation[relationElements.size()];
660                String fromTable = table;
661                for ( int i = 0; i < tableRelations.length; i++ ) {
662                    tableRelations[i] = parseTableRelation( (Element) relationElements.get( i ), fromTable );
663                    fromTable = tableRelations[i].getToTable();
664                }
665    
666                pt = new MappedGeometryPropertyType( propertyName, typeName, type, minOccurs, maxOccurs, isIdentityPart,
667                                                     srs, tableRelations, mappingField );
668            } catch ( XMLParsingException e ) {
669                throw new XMLSchemaException( "Error in definition of geometry property '" + propertyName + "': "
670                                              + e.getMessage() );
671            }
672            return pt;
673        }
674    
675        /**
676         * Extracts the mapping information for a feature property type from the given "xs:annotation" element.
677         *
678         * @param element
679         *            "xs:annotation" element
680         * @param propertyName
681         *            name of the property (local part is used as default field name if it is not specified explicitly in
682         *            the "MappingField" element)
683         * @param minOccurs
684         * @param maxOccurs
685         * @param isIdentityPart
686         * @param table
687         *            name of the table that is associated with the feature type that this property belongs to
688         * @param isReferenceType
689         *            true, if this property is of type "gml:ReferenceType", false otherwise
690         * @return feature property type with persistence information
691         * @throws XMLSchemaException
692         *             if a syntactic or semantic error is found
693         */
694        MappedFeaturePropertyType parseMappedFeaturePropertyType( Element element, QualifiedName propertyName,
695                                                                  int minOccurs, int maxOccurs, boolean isIdentityPart,
696                                                                  String table, boolean isReferenceType )
697                                throws XMLSchemaException {
698    
699            MappedFeaturePropertyType pt = null;
700            try {
701                Element contentElement = (Element) XMLTools.getRequiredNode( element, "xs:appinfo/deegreewfs:Content",
702                                                                             nsContext );
703    
704                // sanity check (common error)
705                Node mfNode = XMLTools.getNode( element, "deegreewfs:MappingField", nsContext );
706                if ( mfNode != null ) {
707                    throw new XMLParsingException( "Content element must not contain a MappingField element." );
708                }
709    
710                QualifiedName containedFT = parseQualifiedName( XMLTools.getRequiredNode( contentElement, "@type",
711                                                                                          nsContext ) );
712                MappedFeatureTypeReference containedFTRef = new MappedFeatureTypeReference( containedFT );
713    
714                List<Node> relationElements = XMLTools.getNodes( contentElement, "deegreewfs:Relation", nsContext );
715                TableRelation[] tableRelations = new TableRelation[relationElements.size()];
716                String fromTable = table;
717                for ( int i = 0; i < tableRelations.length; i++ ) {
718                    tableRelations[i] = parseTableRelation( (Element) relationElements.get( i ), fromTable );
719                    fromTable = tableRelations[i].getToTable();
720                }
721    
722                boolean allowExternalLinks = getNodeAsBoolean( contentElement, "@allowExternalLinks", nsContext, false );
723    
724                pt = new MappedFeaturePropertyType( propertyName, Types.FEATURE_PROPERTY_NAME, Types.FEATURE, minOccurs,
725                                                    maxOccurs, isIdentityPart, tableRelations, containedFTRef,
726                                                    isReferenceType, allowExternalLinks );
727            } catch ( XMLParsingException e ) {
728                throw new XMLSchemaException( "Error in definition of feature property '" + propertyName + "': "
729                                              + e.getMessage() );
730            }
731            return pt;
732        }
733    
734        /**
735         * Parses the given 'MappingField' element.
736         *
737         * @param element
738         *            'MappingField' element
739         * @param defaultField
740         *            if null, the element must have a 'field' attribute, otherwise the given value is used, if the element
741         *            misses a 'field' attribute
742         * @param defaultTable
743         *            if null, the element must have a 'table' attribute, otherwise the 'table' attribute must be left out
744         *            or match the given value
745         * @return class representation of 'MappingField' element
746         * @throws XMLParsingException
747         *             if a syntactic or semantic error is found
748         */
749        private MappingField parseMappingField( Element element, String defaultField, String defaultTable )
750                                throws XMLParsingException {
751    
752            MappingField mappingField = null;
753    
754            String field = null;
755            if ( defaultField == null ) {
756                field = XMLTools.getRequiredNodeAsString( element, "@field", nsContext );
757            } else {
758                field = XMLTools.getNodeAsString( element, "@field", nsContext, defaultField );
759            }
760    
761            String typeName = XMLTools.getRequiredNodeAsString( element, "@type", nsContext );
762            int typeCode = -1;
763            try {
764                typeCode = Types.getTypeCodeForSQLType( typeName );
765            } catch ( UnknownTypeException e ) {
766                throw new XMLParsingException( "Invalid field type: " + e.getMessage(), e );
767            }
768    
769            String table = null;
770            if ( defaultTable == null ) {
771                // if table is unspecified, this is resolved later (in MappedGMLSchema)
772                // TODO clean this up
773                table = XMLTools.getNodeAsString( element, "@table", nsContext, null );
774            } else {
775                table = XMLTools.getNodeAsString( element, "@table", nsContext, defaultTable );
776                if ( !table.equals( defaultTable ) ) {
777                    throw new XMLParsingException( "Specified 'table' attribute ('" + table
778                                                   + "') in 'MappingField' element is inconsistent; leave out or use '"
779                                                   + defaultTable + "' instead." );
780                }
781            }
782    
783            boolean auto = XMLTools.getNodeAsBoolean( element, "@auto", nsContext, false );
784            mappingField = new MappingField( table, field, typeCode, auto );
785    
786            return mappingField;
787        }
788    
789        /**
790         * Parses the given 'MappingField' element.
791         *
792         * @param element
793         *            'MappingField' element
794         * @param defaultField
795         *            if null, the element must have a 'field' attribute, otherwise the given value is used, if the element
796         *            misses a 'field' attribute
797         * @param defaultTable
798         *            if null, the element must have a 'table' attribute, otherwise the 'table' attribute must be left out
799         *            or match the given value
800         * @return class representation of 'MappingField' element
801         * @throws XMLParsingException
802         *             if a syntactic or semantic error is found
803         */
804        private MappingGeometryField parseGeometryMappingField( Element element, String defaultField, String defaultTable )
805                                throws XMLParsingException {
806    
807            MappingGeometryField mappingField = null;
808    
809            String field = null;
810            if ( defaultField == null ) {
811                field = XMLTools.getRequiredNodeAsString( element, "@field", nsContext );
812            } else {
813                field = XMLTools.getNodeAsString( element, "@field", nsContext, defaultField );
814            }
815    
816            String typeName = XMLTools.getRequiredNodeAsString( element, "@type", nsContext );
817            int typeCode = Types.OTHER;
818            if ( !( "GEOMETRY".equals( typeName ) ) ) {
819                try {
820                    typeCode = Types.getTypeCodeForSQLType( typeName );
821                } catch ( UnknownTypeException e ) {
822                    throw new XMLParsingException( "Invalid field type: " + e.getMessage(), e );
823                }
824            }
825    
826            String table = null;
827            if ( defaultTable == null ) {
828                // if table is unspecified, this is resolved later (in MappedGMLSchema)
829                // TODO clean this up
830                table = XMLTools.getNodeAsString( element, "@table", nsContext, null );
831            } else {
832                table = XMLTools.getNodeAsString( element, "@table", nsContext, defaultTable );
833                if ( !table.equals( defaultTable ) ) {
834                    String msg = "Specified 'table' attribute ('" + table
835                                 + "') in 'MappingField' element is inconsistent; leave out or use '" + defaultTable
836                                 + "' instead.";
837                    throw new XMLParsingException( msg );
838                }
839            }
840    
841            int internalSrs = XMLTools.getNodeAsInt( element, "@srs", nsContext, -1 );
842            mappingField = new MappingGeometryField( table, field, typeCode, internalSrs );
843            return mappingField;
844        }
845    
846        /**
847         * Parses the given 'gmlId' element.
848         *
849         * @param element
850         *            'gmlId' element
851         * @param table
852         *            the associated table of the FeatureType
853         * @return class representation of 'gmlId' element
854         * @throws XMLParsingException
855         *             if a syntactic or semantic error is found
856         */
857        private MappedGMLId parseGMLIdDefinition( Element element, String table )
858                                throws XMLParsingException {
859            String prefix = XMLTools.getNodeAsString( element, "@prefix", nsContext, "" );
860            String separator = XMLTools.getNodeAsString( element, "@separator", nsContext, "" );
861    
862            List<Node> mappingFieldElementList = XMLTools.getRequiredNodes( element, "deegreewfs:MappingField", nsContext );
863            MappingField[] mappingFields = new MappingField[mappingFieldElementList.size()];
864            for ( int i = 0; i < mappingFields.length; i++ ) {
865                Element mappingFieldElement = (Element) mappingFieldElementList.get( i );
866                mappingFields[i] = parseMappingField( mappingFieldElement,
867                                                      XMLTools.getRequiredNodeAsString( mappingFieldElement, "@field",
868                                                                                        nsContext ), table );
869            }
870    
871            IDPART_INFO idpart_info = IDPART_INFO.noIDInfo;
872            String identityPart = XMLTools.getNodeAsString( element, "deegreewfs:IdentityPart/text()", nsContext, null );
873            if ( identityPart != null ) {
874                if ( "false".equals( identityPart ) ) {
875                    idpart_info = IDPART_INFO.notIDPart;
876                } else {
877                    idpart_info = IDPART_INFO.isIDPart;
878                }
879            }
880    
881            IdGenerator idGenerator = null;
882            Element idGeneratorElement = (Element) XMLTools.getNode( element, "deegreewfs:IdGenerator", nsContext );
883            if ( idGeneratorElement != null ) {
884                idGenerator = parseGMLIdGenerator( idGeneratorElement );
885            } else {
886                idGenerator = IdGenerator.getInstance( IdGenerator.TYPE_UUID, new Properties() );
887            }
888            return new MappedGMLId( prefix, separator, mappingFields, idGenerator, idpart_info );
889        }
890    
891        /**
892         * Parses the given 'IdGenerator' element.
893         *
894         * @param element
895         *            'IdGenerator' element
896         * @return object representation of 'IdGenerator' element
897         * @throws XMLParsingException
898         *             if a syntactic or semantic error is found
899         */
900        private IdGenerator parseGMLIdGenerator( Element element )
901                                throws XMLParsingException {
902            String type = XMLTools.getRequiredNodeAsString( element, "@type", nsContext );
903            Properties params = new Properties();
904            List<Node> paramElementList = XMLTools.getNodes( element, "deegreewfs:param", nsContext );
905            Iterator<Node> iter = paramElementList.iterator();
906            while ( iter.hasNext() ) {
907                Element paramElement = (Element) iter.next();
908                String name = XMLTools.getRequiredNodeAsString( paramElement, "@name", nsContext );
909                String value = XMLTools.getRequiredNodeAsString( paramElement, "text()", nsContext );
910                params.setProperty( name, value );
911            }
912            IdGenerator idGenerator = IdGenerator.getInstance( type, params );
913            return idGenerator;
914        }
915    
916        private TableRelation parseTableRelation( Element element, String fromTable )
917                                throws XMLParsingException {
918            List<Node> fromMappingElements = XMLTools.getRequiredNodes( element, "deegreewfs:From/deegreewfs:MappingField",
919                                                                        nsContext );
920            List<Node> toMappingElements = XMLTools.getRequiredNodes( element, "deegreewfs:To/deegreewfs:MappingField",
921                                                                      nsContext );
922            if ( fromMappingElements.size() != toMappingElements.size() ) {
923                throw new XMLParsingException( "Error in 'Relation' element: number of 'MappingField' elements "
924                                               + "below 'From' and 'To' elements do not match." );
925            }
926            FK_INFO fkInfo = FK_INFO.noFKInfo;
927            boolean fromIsFK = XMLTools.getNodeAsBoolean( element, "deegreewfs:From/@fk", nsContext, false );
928            boolean toIsFK = XMLTools.getNodeAsBoolean( element, "deegreewfs:To/@fk", nsContext, false );
929            if ( fromIsFK && toIsFK ) {
930                throw new XMLParsingException( "Error in 'Relation' element: either 'To' or 'From' can "
931                                               + "have a 'fk' attribute with value 'true', but not both." );
932            }
933            if ( fromIsFK ) {
934                fkInfo = FK_INFO.fkIsFromField;
935            }
936            if ( toIsFK ) {
937                fkInfo = FK_INFO.fkIsToField;
938            }
939            MappingField[] fromMappingFields = new MappingField[fromMappingElements.size()];
940            MappingField[] toMappingFields = new MappingField[fromMappingFields.length];
941            for ( int i = 0; i < fromMappingFields.length; i++ ) {
942                fromMappingFields[i] = parseMappingField( (Element) fromMappingElements.get( i ), null, fromTable );
943                toMappingFields[i] = parseMappingField( (Element) toMappingElements.get( i ), null, null );
944            }
945    
946            // parse id generator
947            // TODO sanity checks
948            IdGenerator idGenerator = null;
949            if ( fromIsFK ) {
950                Element idGeneratorElement = (Element) XMLTools.getNode( element, "deegreewfs:To/deegreewfs:IdGenerator",
951                                                                         nsContext );
952                if ( idGeneratorElement != null ) {
953                    idGenerator = parseGMLIdGenerator( idGeneratorElement );
954                } else {
955                    idGenerator = IdGenerator.getInstance( IdGenerator.TYPE_UUID, new Properties() );
956                }
957            } else {
958                Element idGeneratorElement = (Element) XMLTools.getNode( element, "deegreewfs:From/deegreewfs:IdGenerator",
959                                                                         nsContext );
960                if ( idGeneratorElement != null ) {
961                    idGenerator = parseGMLIdGenerator( idGeneratorElement );
962                } else {
963                    idGenerator = IdGenerator.getInstance( IdGenerator.TYPE_UUID, new Properties() );
964                }
965            }
966    
967            return new TableRelation( fromMappingFields, toMappingFields, fkInfo, idGenerator );
968        }
969    
970        /**
971         * Returns the value of the "deegreewfs:visible" element.
972         *
973         * @param annotationElement
974         * @return -1 if it is not present, 0 if it is "false", 1 if it is "true"
975         * @throws XMLParsingException
976         */
977        public int parseVisible( Element annotationElement )
978                                throws XMLParsingException {
979            int visibleCode = -1;
980            String visible = XMLTools.getNodeAsString( annotationElement, "xs:appinfo/deegreewfs:visible/text()",
981                                                       nsContext, null );
982            if ( visible != null ) {
983                if ( "false".equals( visible ) ) {
984                    visibleCode = 0;
985                } else {
986                    visibleCode = 1;
987                }
988            }
989            return visibleCode;
990        }
991    
992        /**
993         * Parses the 'updatable' status of the given feature type annotation element.
994         *
995         * @param annotationElement
996         * @return true, if update transactions may be performed on the feature type, false otherwise
997         * @throws XMLParsingException
998         */
999        public boolean parseIsUpdatable( Element annotationElement )
1000                                throws XMLParsingException {
1001            return XMLTools.getNodeAsBoolean( annotationElement, "xs:appinfo/deegreewfs:transaction/@update", nsContext,
1002                                              false );
1003        }
1004    
1005        /**
1006         * Parses the 'deletable' status of the given feature type annotation element.
1007         *
1008         * @param annotationElement
1009         * @return true, if delete transactions may be performed on the feature type, false otherwise
1010         * @throws XMLParsingException
1011         */
1012        public boolean parseIsDeletable( Element annotationElement )
1013                                throws XMLParsingException {
1014            return XMLTools.getNodeAsBoolean( annotationElement, "xs:appinfo/deegreewfs:transaction/@delete", nsContext,
1015                                              false );
1016        }
1017    
1018        /**
1019         * Parses the 'insertable' status of the given feature type annotation element.
1020         *
1021         * @param annotationElement
1022         * @return true, if insert transactions may be performed on the feature type, false otherwise
1023         * @throws XMLParsingException
1024         */
1025        public boolean parseIsInsertable( Element annotationElement )
1026                                throws XMLParsingException {
1027            return XMLTools.getNodeAsBoolean( annotationElement, "xs:appinfo/deegreewfs:transaction/@insert", nsContext,
1028                                              false );
1029        }
1030    
1031        /**
1032         * Parses the 'isPseudoFeatureType' element of the given feature type annotation element.
1033         *
1034         * @param annotationElement
1035         * @return true, if feature type is a pseudo feature type, false otherwise
1036         * @throws XMLParsingException
1037         */
1038        public boolean parseIsPseudoFeatureType( Element annotationElement )
1039                                throws XMLParsingException {
1040            return XMLTools.getNodeAsBoolean( annotationElement, "xs:appinfo/deegreewfs:isPseudoFeatureType/text()",
1041                                              nsContext, false );
1042        }
1043    
1044        /**
1045         * Returns the value of the "deegreewfs:IdentityPart" element.
1046         *
1047         * @param annotationElement
1048         * @return -1 if it is not present, 0 if it is "false", 1 if it is "true"
1049         * @throws XMLParsingException
1050         */
1051        public int parseIdentityPart( Element annotationElement )
1052                                throws XMLParsingException {
1053            int identityCode = -1;
1054            String identityPart = XMLTools.getNodeAsString( annotationElement, "xs:appinfo/deegreewfs:IdentityPart/text()",
1055                                                            nsContext, null );
1056            if ( identityPart != null ) {
1057                if ( "false".equals( identityPart ) ) {
1058                    identityCode = 0;
1059                } else {
1060                    identityCode = 1;
1061                }
1062            }
1063            return identityCode;
1064        }
1065    
1066        /**
1067         * Returns the value of the "deegreewfs:DefaultSRS" element.
1068         *
1069         * @param annotationElement
1070         * @param defaultValue
1071         * @return default SRS of the feature type
1072         * @throws XMLParsingException
1073         */
1074        public URI parseDefaultSRS( Element annotationElement, URI defaultValue )
1075                                throws XMLParsingException {
1076            URI defaultSRS = XMLTools.getNodeAsURI( annotationElement, "xs:appinfo/deegreewfs:DefaultSRS/text()",
1077                                                    nsContext, defaultValue );
1078            return defaultSRS;
1079        }
1080    
1081        /**
1082         * Returns the values of the "deegreewfs:OtherSRS" elements.
1083         *
1084         * @param annotationElement
1085         * @return alternative SRS that may be used to query the feature type
1086         * @throws XMLParsingException
1087         */
1088        public URI[] parseOtherSRS( Element annotationElement )
1089                                throws XMLParsingException {
1090            URI[] otherSRS = XMLTools.getNodesAsURIs( annotationElement, "xs:appinfo/deegreewfs:OtherSRS/text()", nsContext );
1091            return otherSRS;
1092        }
1093    
1094        /**
1095         * @param annotationElement
1096         * @param table
1097         * @param schema
1098         * @return a list of default GML properties to include. The qualified name specified (if not null), after which
1099         *         property this one should be included.
1100         * @throws XMLParsingException
1101         */
1102        public LinkedList<Pair<PropertyType, QualifiedName>> parseGMLDefaultProps( Element annotationElement, String table,
1103                                                                                   MappedGMLSchema schema )
1104                                throws XMLParsingException {
1105            Element appinfo = getRequiredElement( annotationElement, XS_PREFIX + ":appinfo", nsContext );
1106    
1107            LinkedList<Pair<PropertyType, QualifiedName>> list = new LinkedList<Pair<PropertyType, QualifiedName>>();
1108    
1109            if ( appinfo == null ) {
1110                return list;
1111            }
1112    
1113            for ( Element e : getElements( appinfo, DEEGREEWFS_PREFIX + ":StandardGMLProps/" + XS_PREFIX + ":element",
1114                                           nsContext ) ) {
1115                try {
1116                    QualifiedName realName = getRequiredNodeAsQualifiedName( e, "@name", nsContext );
1117                    Pair<PropertyType, QualifiedName> p = new Pair<PropertyType, QualifiedName>();
1118                    p.first = schema.buildPropertyTypeRealName( parseElementDeclaration( e ), table, realName );
1119                    p.second = getNodeAsQualifiedName( e, "@insertAfter", nsContext, null );
1120                    list.add( p );
1121                } catch ( UnknownCRSException e1 ) {
1122                    throw new XMLParsingException( "Wrapped exception", e1 );
1123                }
1124            }
1125    
1126            return list;
1127        }
1128    }