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