001    //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/branches/2.2_testing/src/org/deegree/ogcwebservices/wfs/operation/transaction/TransactionDocument.java $
002    /*----------------    FILE HEADER  ------------------------------------------
003    
004     This file is part of deegree.
005     Copyright (C) 2001-2008 by:
006     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.ogcwebservices.wfs.operation.transaction;
044    
045    import java.io.IOException;
046    import java.net.URI;
047    import java.net.URL;
048    import java.util.ArrayList;
049    import java.util.LinkedHashMap;
050    import java.util.List;
051    import java.util.Map;
052    
053    import org.deegree.datatypes.QualifiedName;
054    import org.deegree.framework.xml.XMLParsingException;
055    import org.deegree.framework.xml.XMLTools;
056    import org.deegree.i18n.Messages;
057    import org.deegree.model.crs.UnknownCRSException;
058    import org.deegree.model.feature.Feature;
059    import org.deegree.model.feature.FeatureCollection;
060    import org.deegree.model.feature.FeatureFactory;
061    import org.deegree.model.feature.FeatureProperty;
062    import org.deegree.model.feature.GMLFeatureCollectionDocument;
063    import org.deegree.model.feature.GMLFeatureDocument;
064    import org.deegree.model.filterencoding.AbstractFilter;
065    import org.deegree.model.filterencoding.Filter;
066    import org.deegree.ogcbase.CommonNamespaces;
067    import org.deegree.ogcbase.PropertyPath;
068    import org.deegree.ogcwebservices.InvalidParameterValueException;
069    import org.deegree.ogcwebservices.wfs.operation.AbstractWFSRequestDocument;
070    import org.deegree.ogcwebservices.wfs.operation.transaction.Insert.ID_GEN;
071    import org.w3c.dom.Element;
072    import org.w3c.dom.Node;
073    import org.w3c.dom.Text;
074    import org.xml.sax.SAXException;
075    
076    /**
077     * Parser for "wfs:Transaction" requests and contained elements.
078     * 
079     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider</a>
080     * @author last edited by: $Author: apoth $
081     * 
082     * @version $Revision: 9345 $, $Date: 2007-12-27 17:22:25 +0100 (Do, 27 Dez 2007) $
083     */
084    public class TransactionDocument extends AbstractWFSRequestDocument {
085    
086        private static final long serialVersionUID = -394478447170286393L;
087    
088        private static final String XML_TEMPLATE = "TransactionTemplate.xml";
089    
090        private static final QualifiedName VALUE_ELEMENT_NAME = new QualifiedName( "wfs", "Value", CommonNamespaces.WFSNS );
091    
092        /**
093         * Creates a skeleton document that contains the root element and the namespace bindings only.
094         * 
095         * @throws IOException
096         * @throws SAXException
097         */
098        public void createEmptyDocument()
099                                throws IOException, SAXException {
100            URL url = TransactionDocument.class.getResource( XML_TEMPLATE );
101            if ( url == null ) {
102                throw new IOException( "The resource '" + XML_TEMPLATE + " could not be found." );
103            }
104            load( url );
105        }
106    
107        /**
108         * Parses the underlying document into a <code>Transaction</code> request object.
109         * 
110         * @param id
111         * @return corresponding <code>Transaction</code> object
112         * @throws XMLParsingException
113         * @throws InvalidParameterValueException
114         */
115        public Transaction parse( String id )
116                                throws XMLParsingException, InvalidParameterValueException {
117    
118            checkServiceAttribute();
119            String version = checkVersionAttribute();
120    
121            Element root = this.getRootElement();
122            String lockId = null;
123            boolean releaseAllFeatures = parseReleaseActionParameter();
124    
125            List<TransactionOperation> operations = new ArrayList<TransactionOperation>();
126            List<Element> list = XMLTools.getElements( root, "*", nsContext );
127            for ( int i = 0; i < list.size(); i++ ) {
128                Element element = list.get( i );
129                if ( "LockId".equals( element.getLocalName() )
130                     && CommonNamespaces.WFSNS.toString().equals( element.getNamespaceURI() ) ) {
131                    lockId = XMLTools.getNodeAsString( element, "text()", nsContext, null );
132                } else {
133                    TransactionOperation operation = parseOperation( element );
134                    operations.add( operation );
135                }
136            }
137    
138            // vendorspecific attributes; required by deegree rights management
139            Map<String, String> vendorSpecificParams = parseDRMParams( root );
140    
141            return new Transaction( id, version, vendorSpecificParams, lockId, operations, releaseAllFeatures, this );
142        }
143    
144        /**
145         * Parses the optional "releaseAction" attribute of the root element.
146         * 
147         * @return true, if releaseAction equals "ALL" (or is left out), false if it equals "SOME"
148         * @throws InvalidParameterValueException
149         *             if parameter
150         * @throws XMLParsingException
151         */
152        private boolean parseReleaseActionParameter()
153                                throws InvalidParameterValueException, XMLParsingException {
154    
155            String releaseAction = XMLTools.getNodeAsString( getRootElement(), "@releaseAction", nsContext, "ALL" );
156            boolean releaseAllFeatures = true;
157            if ( releaseAction != null ) {
158                if ( "SOME".equals( releaseAction ) ) {
159                    releaseAllFeatures = false;
160                } else if ( "ALL".equals( releaseAction ) ) {
161                    releaseAllFeatures = true;
162                } else {
163                    throw new InvalidParameterValueException( "releaseAction", releaseAction );
164                }
165            }
166            return releaseAllFeatures;
167        }
168    
169        /**
170         * Parses the given element as a <code>TransactionOperation</code>.
171         * <p>
172         * The given element must be one of the following:
173         * <ul>
174         * <li>wfs:Insert</li>
175         * <li>wfs:Update</li>
176         * <li>wfs:Delete</li>
177         * <li>wfs:Native</li>
178         * </ul>
179         * 
180         * @param element
181         *            operation element
182         * @return corresponding <code>TransactionOperation</code> object
183         * @throws XMLParsingException
184         */
185        private TransactionOperation parseOperation( Element element )
186                                throws XMLParsingException {
187    
188            TransactionOperation operation = null;
189    
190            if ( !element.getNamespaceURI().equals( CommonNamespaces.WFSNS.toString() ) ) {
191                String msg = Messages.getMessage( "WFS_INVALID_OPERATION", element.getNodeName() );
192                throw new XMLParsingException( msg );
193            }
194            if ( element.getLocalName().equals( "Insert" ) ) {
195                operation = parseInsert( element );
196            } else if ( element.getLocalName().equals( "Update" ) ) {
197                operation = parseUpdate( element );
198            } else if ( element.getLocalName().equals( "Delete" ) ) {
199                operation = parseDelete( element );
200            } else if ( element.getLocalName().equals( "Native" ) ) {
201                operation = parseNative( element );
202            } else {
203                String msg = Messages.getMessage( "WFS_INVALID_OPERATION", element.getNodeName() );
204                throw new XMLParsingException( msg );
205            }
206    
207            return operation;
208        }
209    
210        /**
211         * Parses the given element as a "wfs:Insert" operation.
212         * 
213         * @param element
214         *            "wfs:Insert" operation
215         * @return corresponding Insert object
216         * @throws XMLParsingException
217         */
218        private Insert parseInsert( Element element )
219                                throws XMLParsingException {
220            FeatureCollection fc = null;
221            ID_GEN mode = parseIdGen( element );
222            String handle = XMLTools.getNodeAsString( element, "@handle", nsContext, null );
223            URI srsName = XMLTools.getNodeAsURI( element, "@srsName", nsContext, null );
224            List<Element> childElementList = XMLTools.getRequiredElements( element, "*", nsContext );
225    
226            // either one _gml:FeatureCollection element or any number of _gml:Feature elements
227            boolean isFeatureCollection = isFeatureCollection( childElementList.get( 0 ) );
228    
229            if ( isFeatureCollection ) {
230                LOG.logDebug( "Insert (FeatureCollection)" );
231                GMLFeatureCollectionDocument doc = new GMLFeatureCollectionDocument( false );
232                doc.setRootElement( childElementList.get( 0 ) );
233                doc.setSystemId( this.getSystemId() );
234                fc = doc.parse();
235            } else {
236                LOG.logDebug( "Insert (Features)" );
237                Feature[] features = new Feature[childElementList.size()];
238                for ( int i = 0; i < childElementList.size(); i++ ) {
239                    try {
240                        GMLFeatureDocument doc = new GMLFeatureDocument( false );
241                        doc.setRootElement( childElementList.get( i ) );
242                        doc.setSystemId( this.getSystemId() );
243                        features[i] = doc.parseFeature();
244                    } catch ( Exception e ) {
245                        throw new XMLParsingException( e.getMessage(), e );
246                    }
247                }
248                fc = FeatureFactory.createFeatureCollection( null, features );
249            }
250    
251            return new Insert( handle, mode, srsName, fc );
252        }
253    
254        /**
255         * Checks whether the given element is a (concrete) gml:_FeatureCollection element.
256         * <p>
257         * NOTE: This check is far from perfect. Instead of determining the type of the element by inspecting the schema,
258         * the decision is made by checking for child elements with name "gml:featureMember".
259         * 
260         * @param element
261         *            potential gml:_FeatureCollection element
262         * @return true, if the given element appears to be a gml:_FeatureCollection element, false otherwise
263         * @throws XMLParsingException
264         */
265        private boolean isFeatureCollection( Element element )
266                                throws XMLParsingException {
267            boolean containsFeatureCollection = false;
268            List<Node> nodeList = XMLTools.getNodes( element, "gml:featureMember", nsContext );
269            if ( nodeList.size() > 0 ) {
270                containsFeatureCollection = true;
271            }
272            return containsFeatureCollection;
273        }
274    
275        /**
276         * Parses the optional "idGen" attribute of the given "wfs:Insert" element.
277         * 
278         * @param element
279         *            "wfs:Insert" element
280         * @return "idGen" attribute code
281         * @throws XMLParsingException
282         */
283        private ID_GEN parseIdGen( Element element )
284                                throws XMLParsingException {
285            ID_GEN mode;
286            String idGen = XMLTools.getNodeAsString( element, "@idgen", nsContext, Insert.ID_GEN_GENERATE_NEW_STRING );
287    
288            if ( Insert.ID_GEN_GENERATE_NEW_STRING.equals( idGen ) ) {
289                mode = Insert.ID_GEN.GENERATE_NEW;
290            } else if ( Insert.ID_GEN_USE_EXISTING_STRING.equals( idGen ) ) {
291                mode = Insert.ID_GEN.USE_EXISTING;
292            } else if ( Insert.ID_GEN_REPLACE_DUPLICATE_STRING.equals( idGen ) ) {
293                mode = Insert.ID_GEN.REPLACE_DUPLICATE;
294            } else {
295                String msg = Messages.getMessage( "WFS_INVALID_IDGEN_VALUE", idGen, Insert.ID_GEN_GENERATE_NEW_STRING,
296                                                  Insert.ID_GEN_REPLACE_DUPLICATE_STRING, Insert.ID_GEN_USE_EXISTING_STRING );
297                throw new XMLParsingException( msg );
298            }
299            return mode;
300        }
301    
302        /**
303         * Parses the given element as a "wfs:Update" operation.
304         * 
305         * @param element
306         *            "wfs:Update" operation
307         * @return corresponding Update object
308         * @throws XMLParsingException
309         */
310        private Update parseUpdate( Element element )
311                                throws XMLParsingException {
312    
313            Update update = null;
314            String handle = XMLTools.getNodeAsString( element, "@handle", nsContext, null );
315            QualifiedName typeName = XMLTools.getRequiredNodeAsQualifiedName( element, "@typeName", nsContext );
316    
317            Element filterElement = (Element) XMLTools.getNode( element, "ogc:Filter", nsContext );
318            Filter filter = null;
319            if ( filterElement != null ) {
320                filter = AbstractFilter.buildFromDOM( filterElement );
321            }
322    
323            List<Node> properties = XMLTools.getNodes( element, "wfs:Property", nsContext );
324            if ( properties.size() > 0 ) {
325                // "standard" update (specifies properties + their replacement values)
326                LOG.logDebug( "Update (replacement properties)" );
327                Map<PropertyPath, FeatureProperty> replacementProps = new LinkedHashMap<PropertyPath, FeatureProperty>();
328                Map<PropertyPath, Node> rawProps = new LinkedHashMap<PropertyPath, Node>();
329                for ( int i = 0; i < properties.size(); i++ ) {
330                    Node propNode = properties.get( i );
331                    Text propNameNode = (Text) XMLTools.getRequiredNode( propNode, "wfs:Name/text()", nsContext );
332                    PropertyPath propPath = parsePropertyPath( propNameNode );
333    
334                    // TODO remove this (only needed, because Update makes the DOM representation available)
335                    Node valueNode = XMLTools.getNode( propNode, "wfs:Value/*", nsContext );
336                    if ( valueNode == null ) {
337                        valueNode = XMLTools.getNode( propNode, "wfs:Value/text()", nsContext );
338                    }
339                    rawProps.put( propPath, propNode );
340    
341                    FeatureProperty replacementProp = null;
342                    QualifiedName propName = propPath.getStep( propPath.getSteps() - 1 ).getPropertyName();
343    
344                    if ( replacementProps.get( propPath ) != null ) {
345                        String msg = Messages.getMessage( "WFS_UPDATE_DUPLICATE_PROPERTY", handle, propPath );
346                        throw new XMLParsingException( msg );
347                    }
348    
349                    // TODO improve generation of FeatureProperty from "wfs:Value" element
350                    Object propValue = null;
351                    Node node = XMLTools.getNode( propNode, "wfs:Value", nsContext );
352                    if ( node != null ) {
353                        GMLFeatureDocument dummyDoc = new GMLFeatureDocument( false );
354                        dummyDoc.setRootElement( (Element) propNode );
355                        dummyDoc.setSystemId( this.getSystemId() );
356                        Feature dummyFeature;
357                        try {
358                            dummyFeature = dummyDoc.parseFeature();
359                        } catch ( UnknownCRSException e ) {
360                            throw new XMLParsingException( e.getMessage(), e );
361                        }
362                        propValue = dummyFeature.getProperties( VALUE_ELEMENT_NAME )[0].getValue();
363                    }
364                    replacementProp = FeatureFactory.createFeatureProperty( propName, propValue );
365                    replacementProps.put( propPath, replacementProp );
366                }
367                update = new Update( handle, typeName, replacementProps, rawProps, filter );
368            } else {
369                // deegree specific update (specifies a single replacement feature)
370                LOG.logDebug( "Update (replacement feature)" );
371                Feature replacementFeature = null;
372                List<Element> childElementList = XMLTools.getRequiredElements( element, "*", nsContext );
373                if ( ( filter == null && childElementList.size() != 1 )
374                     || ( filter != null && childElementList.size() != 2 ) ) {
375                    String msg = Messages.getMessage( "WFS_UPDATE_FEATURE_REPLACE", handle );
376                    throw new XMLParsingException( msg );
377                }
378                try {
379                    GMLFeatureDocument doc = new GMLFeatureDocument( false );
380                    doc.setRootElement( childElementList.get( 0 ) );
381                    doc.setSystemId( this.getSystemId() );
382                    replacementFeature = doc.parseFeature();
383                } catch ( Exception e ) {
384                    String msg = Messages.getMessage( "WFS_UPDATE_FEATURE_REPLACE", handle );
385                    throw new XMLParsingException( msg );
386                }
387                update = new Update( handle, typeName, replacementFeature, filter );
388            }
389    
390            return update;
391        }
392    
393        /**
394         * Parses the given element as a "wfs:Delete" operation.
395         * 
396         * @param element
397         *            "wfs:Delete" operation
398         * @return corresponding Delete object
399         */
400        private Delete parseDelete( Element element )
401                                throws XMLParsingException {
402    
403            String handle = XMLTools.getNodeAsString( element, "@handle", nsContext, null );
404            QualifiedName typeName = XMLTools.getRequiredNodeAsQualifiedName( element, "@typeName", nsContext );
405    
406            Element filterElement = (Element) XMLTools.getRequiredNode( element, "ogc:Filter", nsContext );
407            Filter filter = AbstractFilter.buildFromDOM( filterElement );
408            return new Delete( handle, typeName, filter );
409        }
410    
411        /**
412         * Parses the given element as a "wfs:Native" operation.
413         * 
414         * @param element
415         *            "wfs:Native" operation
416         * @return corresponding Native object
417         */
418        private Native parseNative( Element element )
419                                throws XMLParsingException {
420            String handle = XMLTools.getNodeAsString( element, "@handle", nsContext, null );
421            String vendorID = XMLTools.getRequiredNodeAsString( element, "@vendorId", nsContext );
422            boolean safeToIgnore = XMLTools.getRequiredNodeAsBoolean( element, "@safeToIgnore", nsContext );
423            return new Native( handle, element, vendorID, safeToIgnore );
424        }
425    }