001    //$Header: /deegreerepository/deegree/src/org/deegree/ogcwebservices/wfs/operation/transaction/Transaction.java,v 1.11 2007/02/07 15:01:51 poth Exp $
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 java.net.URI;
039    import java.util.ArrayList;
040    import java.util.HashSet;
041    import java.util.Iterator;
042    import java.util.List;
043    import java.util.Map;
044    import java.util.Set;
045    
046    import org.deegree.datatypes.QualifiedName;
047    import org.deegree.framework.log.ILogger;
048    import org.deegree.framework.log.LoggerFactory;
049    import org.deegree.framework.util.KVP2Map;
050    import org.deegree.framework.xml.XMLParsingException;
051    import org.deegree.model.feature.FeatureFactory;
052    import org.deegree.model.feature.FeatureProperty;
053    import org.deegree.model.filterencoding.Filter;
054    import org.deegree.ogcbase.PropertyPath;
055    import org.deegree.ogcbase.PropertyPathStep;
056    import org.deegree.ogcwebservices.InconsistentRequestException;
057    import org.deegree.ogcwebservices.InvalidParameterValueException;
058    import org.deegree.ogcwebservices.MissingParameterValueException;
059    import org.deegree.ogcwebservices.OGCWebServiceException;
060    import org.deegree.ogcwebservices.wfs.WFService;
061    import org.deegree.ogcwebservices.wfs.operation.AbstractWFSRequest;
062    import org.w3c.dom.Element;
063    
064    /**
065     * Represents a <code>Transaction</code> request to a web feature service.
066     * <p>
067     * A <code>Transaction</code> consists of a sequence of {@link Insert}, {@link Update}, {@link Delete} and
068     * {@link Native} operations.
069     * <p>
070     * From the WFS Specification 1.1.0 OGC 04-094 (#12, Pg.63):
071     * <p>
072     * A <code>Transaction</code> request is used to describe data transformation operations that are to be applied to web
073     * accessible feature instances. When the transaction has been completed, a web feature service will generate an XML
074     * response document indicating the completion status of the transaction.
075     * 
076     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider </a>
077     * @author last edited by: $Author: aionita $
078     * 
079     * @version $Revision: 23794 $, $Date: 2010-04-23 15:05:33 +0200 (Fr, 23 Apr 2010) $
080     */
081    public class Transaction extends AbstractWFSRequest {
082    
083        private static final long serialVersionUID = 6904739857311368390L;
084    
085        private static final ILogger LOG = LoggerFactory.getLogger( Transaction.class );
086    
087        private List<TransactionOperation> operations;
088    
089        // request version
090        private String version;
091    
092        // transaction ID
093        private String id;
094    
095        // LockID associated with the request
096        private String lockId;
097    
098        /**
099         * Specifies if ALL records should be released or if SOME records, indicating only those records which have been
100         * modified will be released. The default is ALL.
101         */
102        private RELEASE_ACTION releaseAction = RELEASE_ACTION.ALL;
103    
104        private TransactionDocument sourceDocument;
105    
106        /** Controls how locked features are treated when a transaction request is completed. */
107        public static enum RELEASE_ACTION {
108    
109            /**
110             * Indicates that the locks on all feature instances locked using the associated lockId should be released when
111             * the transaction completes, regardless of whether or not a particular feature instance in the locked set was
112             * actually operated upon.
113             */
114            ALL,
115    
116            /**
117             * Indicates that only the locks on feature instances modified by the transaction should be released. The other,
118             * unmodified, feature instances should remain locked using the same lockId so that subsequent transactions can
119             * operate on those feature instances. If an expiry period was specified, the expiry counter must be reset to
120             * zero after each transaction unless all feature instances in the locked set have been operated upon.
121             */
122            SOME
123        }
124    
125        /**
126         * Creates a new <code>Transaction</code> instance.
127         * 
128         * @param version
129         *            WFS version
130         * @param id
131         *            Transaction id
132         * @param versionSpecificParameter
133         * @param lockID
134         *            Lock Id
135         * @param operations
136         *            List of operations to be carried out
137         * @param releaseAllFeatures
138         * @param sourceDocument
139         */
140        public Transaction( String id, String version, Map<String, String> versionSpecificParameter, String lockID,
141                            List<TransactionOperation> operations, boolean releaseAllFeatures,
142                            TransactionDocument sourceDocument ) {
143            super( version, id, null, versionSpecificParameter );
144            this.id = id;
145            this.version = version;
146            this.lockId = lockID;
147            this.operations = operations;
148            if ( !releaseAllFeatures ) {
149                this.releaseAction = RELEASE_ACTION.SOME;
150            }
151            this.sourceDocument = sourceDocument;
152        }
153    
154        /**
155         * Returns the source document that was used to create this <code>Transaction</code> instance.
156         * 
157         * @return the source document
158         */
159        public TransactionDocument getSourceDocument() {
160            return this.sourceDocument;
161        }
162    
163        /**
164         * Returns the {@link TransactionOperation}s that are contained in the transaction.
165         * 
166         * @return the contained operations
167         */
168        public List<TransactionOperation> getOperations() {
169            return this.operations;
170        }
171    
172        /**
173         * Returns the lock identifier associated with this transaction.
174         * 
175         * @return the lock identifier associated with this transaction if it exists, null otherwise
176         */
177        public String getLockId() {
178            return this.lockId;
179        }
180    
181        /**
182         * Returns the release action mode to be applied after the transaction finished successfully.
183         * 
184         * @see RELEASE_ACTION
185         * @return the release action mode to be applied after the transaction finished successfully
186         */
187        public RELEASE_ACTION getReleaseAction() {
188            return this.releaseAction;
189        }
190    
191        /**
192         * Returns the names of the feature types that are affected by the transaction.
193         * 
194         * @return the names of the affected feature types
195         */
196        public Set<QualifiedName> getAffectedFeatureTypes() {
197            Set<QualifiedName> featureTypeSet = new HashSet<QualifiedName>();
198    
199            Iterator<TransactionOperation> iter = this.operations.iterator();
200            while ( iter.hasNext() ) {
201                TransactionOperation operation = iter.next();
202                featureTypeSet.addAll( operation.getAffectedFeatureTypes() );
203            }
204            return featureTypeSet;
205        }
206    
207        /**
208         * Creates a <code>Transaction</code> request from a key-value-pair encoding of the parameters contained in the
209         * passed variable 'request'.
210         * 
211         * @param id
212         *            id of the request
213         * @param request
214         *            key-value-pair encoded GetFeature request
215         * @return new created Transaction instance
216         * @throws InconsistentRequestException
217         * @throws InvalidParameterValueException
218         * @throws MissingParameterValueException
219         */
220        public static Transaction create( String id, String request )
221                                throws InconsistentRequestException, InvalidParameterValueException,
222                                MissingParameterValueException {
223    
224            Map<String, String> model = KVP2Map.toMap( request );
225            model.put( "ID", id );
226    
227            return create( model );
228        }
229    
230        /**
231         * Creates a <code>Transaction</code> request from a key-value-pair encoding of the parameters contained in the
232         * given Map.
233         * 
234         * @param model
235         *            key-value-pair encoded Transaction request
236         * @return new Transaction instance
237         * @throws InconsistentRequestException
238         * @throws InvalidParameterValueException
239         * @throws MissingParameterValueException
240         */
241        public static Transaction create( Map<String, String> model )
242                                throws InconsistentRequestException, InvalidParameterValueException,
243                                MissingParameterValueException {
244    
245            Map<String, String> versionSpecificParameter = null;
246    
247            String id = model.get( "ID" );
248    
249            String version = checkVersionParameter( model );
250    
251            checkServiceParameter( model );
252    
253            String request = model.remove( "REQUEST" );
254            if ( request == null ) {
255                throw new InconsistentRequestException( "Request parameter for a transaction request must be set." );
256            }
257    
258            String lockID = model.remove( "LOCKID" );
259    
260            String releaseAction = model.remove( "RELEASEACTION" );
261            boolean releaseAllFeatures = true;
262            if ( releaseAction != null ) {
263                if ( "SOME".equals( releaseAction ) ) {
264                    releaseAllFeatures = false;
265                } else if ( "ALL".equals( releaseAction ) ) {
266                    releaseAllFeatures = true;
267                } else {
268                    throw new InvalidParameterValueException( "releaseAction", releaseAction );
269                }
270            }
271    
272            QualifiedName[] typeNames = extractTypeNames( model );
273    
274            String featureIdParameter = model.remove( "FEATUREID" );
275            if ( typeNames == null && featureIdParameter == null ) {
276                throw new InconsistentRequestException( "TypeName OR FeatureId parameter must be set." );
277            }
278    
279            // String[] featureIds = null;
280            // if ( featureIdParameter != null ) {
281            // // FEATUREID specified. Looking for featureId
282            // // declaration TYPENAME contained in featureId declaration (eg.
283            // // FEATUREID=InWaterA_1M.1013)
284            // featureIds = StringTools.toArray( featureIdParameter, ",", false );
285            // //typeNameSet = extractTypeNameFromFeatureId( featureIds, context, (HashSet) typeNameSet
286            // );
287            // }
288    
289            // Filters
290            // Map typeFilter = buildFilterMap( model, typeNames, featureIds, context );
291    
292            // // BBOX
293            // typeFilter = extractBBOXParameter( model, typeNames, typeFilter );
294            //
295            // if ( typeFilter == null || typeFilter.size() == 0 ) {
296            // for ( int i = 0; i < typeNames.length; i++ ) {
297            // typeFilter.put( typeNames[i], null );
298            // }
299            // }
300    
301            List<TransactionOperation> operations = extractOperations( model, null );
302    
303            return new Transaction( id, version, versionSpecificParameter, lockID, operations, releaseAllFeatures, null );
304        }
305    
306        /**
307         * Extracts the {@link TransactionOperation}s contained in the given kvp request.
308         * 
309         * @param model
310         * @param typeFilter
311         * @return List
312         * @throws InconsistentRequestException
313         */
314        private static List<TransactionOperation> extractOperations( Map<String, String> model,
315                                                                     Map<QualifiedName, Filter> typeFilter )
316                                throws InconsistentRequestException {
317            List<TransactionOperation> operation = new ArrayList<TransactionOperation>();
318            String op = model.remove( "OPERATION" );
319            if ( op == null ) {
320                throw new InconsistentRequestException( "Operation parameter must be set" );
321            }
322            if ( op.equals( "Delete" ) ) {
323                List<Delete> deletes = Delete.create( typeFilter );
324                operation.addAll( deletes );
325            } else {
326                String msg = "Invalid OPERATION parameter '" + op
327                             + "'. KVP Transactions only support the 'Delete' operation.";
328                throw new InconsistentRequestException( msg );
329            }
330            return operation;
331        }
332    
333        /**
334         * Creates a <code>Transaction</code> instance from a document that contains the DOM representation of the request.
335         * 
336         * @param id
337         * @param root
338         *            element that contains the DOM representation of the request
339         * @return transaction instance
340         * @throws OGCWebServiceException
341         */
342        public static Transaction create( String id, Element root )
343                                throws OGCWebServiceException {
344            TransactionDocument doc = new TransactionDocument();
345            doc.setRootElement( root );
346            Transaction request;
347            try {
348                request = doc.parse( id );
349            } catch ( XMLParsingException e ) {
350                if ( e.getWrapped() != null ) {
351                    throw e.getWrapped();
352                }
353                LOG.logError( e.getMessage(), e );
354                throw new OGCWebServiceException( "Transaction", e.getMessage() );
355            }
356            return request;
357        }
358    
359        @Override
360        public String toString() {
361            String ret = this.getClass().getName();
362            ret += "version: " + this.version + "\n";
363            ret += "id: " + this.id + "\n";
364            ret += "lockID: " + this.lockId + "\n";
365            ret += "operations: \n";
366            for ( int i = 0; i < operations.size(); i++ ) {
367                ret += ( i + ": " + operations.get( i ) + "\n " );
368            }
369            ret += "releaseAllFeatures: " + this.releaseAction;
370            return ret;
371        }
372    
373        /**
374         * Adds missing namespaces in the names of requested feature types.
375         * <p>
376         * If the {@link QualifiedName} of a requested type has a null namespace, the first qualified feature type name of
377         * the given {@link WFService} with the same local name is used instead.
378         * <p>
379         * Note: The method changes this request (the feature type names) and should only be called by the
380         * <code>WFSHandler</code> class.
381         * 
382         * @param wfs
383         *            {@link WFService} instance that is used for the lookup of proper (qualified) feature type names
384         */
385        public void guessMissingNamespaces( WFService wfs ) {
386    
387            Set<QualifiedName> featureNames = wfs.getMappedFeatureTypes().keySet();
388            for ( int j = 0; j < operations.size(); j++ ) {
389                TransactionOperation op = operations.get( j );
390                if ( op instanceof Update ) {
391                    Update update = (Update) op;
392                    QualifiedName tn = update.getTypeName();
393                    if ( tn.getNamespace() == null ) {
394                        QualifiedName newTn = guessTypeNameNamespace( tn, featureNames );
395                        update.setTypeName( newTn );
396                    }
397    
398                    if ( update.getReplacementProperties() != null ) {
399                        Set<PropertyPath> propPaths = update.getReplacementProperties().keySet();
400                        String defaultPrefix = update.getTypeName().getPrefix();
401                        URI defaultNamespace = update.getTypeName().getNamespace();
402                        guessMissingPropertyNamespace( propPaths, defaultPrefix, defaultNamespace, update, j );
403                    }
404    
405                } else if ( op instanceof Delete ) {
406                    Delete delete = (Delete) op;
407                    QualifiedName newTn = guessTypeNameNamespace( delete.getTypeName(), featureNames );
408                    delete.setTypeName( newTn );
409                }
410            }
411        }
412    
413        private void guessMissingPropertyNamespace( Set<PropertyPath> propPaths, String defaultPrefix,
414                                                    URI defaultNamespace, Update update, int j ) {
415            for ( PropertyPath propPath : propPaths ) {
416                for ( int i = 0; i < propPath.getAllSteps().size(); i++ ) {
417                    PropertyPathStep step = propPath.getStep( i );
418                    QualifiedName prop = step.getPropertyName();
419                    if ( prop.getNamespace() == null ) {
420                        // the following retrieves the old values the object involved
421                        Map<PropertyPath, FeatureProperty> oldMap = update.getReplacementProperties();
422                        List<PropertyPathStep> steps = propPath.getAllSteps();
423                        FeatureProperty oldValue = oldMap.get( propPath );
424                        oldMap.remove( propPath );
425                        QualifiedName newQName = new QualifiedName( defaultPrefix, prop.getLocalName(), defaultNamespace );
426    
427                        // replaces old values in the objects involved
428                        step.setPropertyName( newQName );
429                        FeatureProperty newValue = FeatureFactory.createFeatureProperty( newQName, oldValue.getValue() );
430                        steps.set( i, step );
431                        propPath.setSteps( steps );
432                        oldMap.put( propPath, newValue );
433                        update.setReplacementProperties( oldMap );
434                        operations.set( j, update );
435                    }
436                }
437            }
438        }
439    
440        private QualifiedName guessTypeNameNamespace( QualifiedName candidate, Set<QualifiedName> featureNames ) {
441            if ( candidate.getNamespace() == null ) {
442                for ( QualifiedName ftName : featureNames ) {
443                    if ( ftName.getLocalName().equals( candidate.getLocalName() ) ) {
444                        return ftName;
445                    }
446                }
447            }
448            return candidate;
449        }
450    
451    }