001    //$Header: /deegreerepository/deegree/src/org/deegree/ogcwebservices/wfs/TransactionHandler.java,v 1.69 2007/03/14 14:41:43 mschneider 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    
037    package org.deegree.ogcwebservices.wfs;
038    
039    import static java.util.Collections.singletonList;
040    import static org.deegree.ogcwebservices.wfs.operation.transaction.Transaction.RELEASE_ACTION.ALL;
041    import static org.deegree.ogcwebservices.wfs.operation.transaction.Transaction.RELEASE_ACTION.SOME;
042    
043    import java.util.ArrayList;
044    import java.util.HashMap;
045    import java.util.Iterator;
046    import java.util.List;
047    import java.util.Map;
048    import java.util.Set;
049    import java.util.TreeSet;
050    
051    import org.deegree.datatypes.QualifiedName;
052    import org.deegree.framework.log.ILogger;
053    import org.deegree.framework.log.LoggerFactory;
054    import org.deegree.i18n.Messages;
055    import org.deegree.io.datastore.Datastore;
056    import org.deegree.io.datastore.DatastoreException;
057    import org.deegree.io.datastore.DatastoreTransaction;
058    import org.deegree.io.datastore.FeatureId;
059    import org.deegree.io.datastore.LockManager;
060    import org.deegree.io.datastore.MissingLockIdException;
061    import org.deegree.io.datastore.PropertyPathResolver;
062    import org.deegree.io.datastore.idgenerator.FeatureIdAssigner;
063    import org.deegree.io.datastore.schema.MappedFeaturePropertyType;
064    import org.deegree.io.datastore.schema.MappedFeatureType;
065    import org.deegree.io.datastore.schema.MappedGMLSchema;
066    import org.deegree.io.datastore.sql.transaction.SQLTransaction;
067    import org.deegree.model.feature.Feature;
068    import org.deegree.model.feature.FeatureCollection;
069    import org.deegree.model.feature.FeatureProperty;
070    import org.deegree.model.feature.GMLFeatureAdapter;
071    import org.deegree.model.feature.GMLFeatureCollectionDocument;
072    import org.deegree.model.feature.Validator;
073    import org.deegree.model.feature.schema.FeatureType;
074    import org.deegree.model.feature.schema.PropertyType;
075    import org.deegree.ogcbase.PropertyPath;
076    import org.deegree.ogcbase.PropertyPathStep;
077    import org.deegree.ogcwebservices.InvalidParameterValueException;
078    import org.deegree.ogcwebservices.MissingParameterValueException;
079    import org.deegree.ogcwebservices.OGCWebServiceException;
080    import org.deegree.ogcwebservices.wfs.operation.transaction.Delete;
081    import org.deegree.ogcwebservices.wfs.operation.transaction.Insert;
082    import org.deegree.ogcwebservices.wfs.operation.transaction.InsertResults;
083    import org.deegree.ogcwebservices.wfs.operation.transaction.Native;
084    import org.deegree.ogcwebservices.wfs.operation.transaction.Transaction;
085    import org.deegree.ogcwebservices.wfs.operation.transaction.TransactionOperation;
086    import org.deegree.ogcwebservices.wfs.operation.transaction.TransactionResponse;
087    import org.deegree.ogcwebservices.wfs.operation.transaction.Update;
088    import org.deegree.ogcwebservices.wfs.operation.transaction.Insert.ID_GEN;
089    
090    /**
091     * Handler for transaction requests to the {@link WFService}.
092     * <p>
093     * If the used backend does not support atomic transactions, it is possible that one part fails while another works
094     * well. Depending on definitions made in the OGC WFS 1.1.0 specification in this case it is possible that even if a sub
095     * part of the request fails no exception will be thrown. In this case the result objects contains informations on the
096     * parts of the request that worked and that did not.
097     * 
098     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider </a>
099     * @author <a href="mailto:deshmukh@lat-lon.de">Anup Deshmukh </a>
100     * @author last edited by: $Author: aschmitz $
101     * 
102     * @version $Revision: 24604 $, $Date: 2010-05-27 11:59:30 +0200 (Do, 27 Mai 2010) $
103     */
104    class TransactionHandler {
105    
106        private static final ILogger LOG = LoggerFactory.getLogger( TransactionHandler.class );
107    
108        private WFService service;
109    
110        private Transaction request;
111    
112        private Map<QualifiedName, MappedFeatureType> ftMap;
113    
114        // filled by #acquireDSTransactions()
115        private Map<QualifiedName, DatastoreTransaction> taMap = new HashMap<QualifiedName, DatastoreTransaction>();
116    
117        // filled by #acquireDSTransactions()
118        private Map<Datastore, DatastoreTransaction> dsToTaMap = new HashMap<Datastore, DatastoreTransaction>();
119    
120        private Set<FeatureId> changedFIDs = new TreeSet<FeatureId>();
121    
122        /**
123         * Creates a new <code>TransactionHandler</code> instance.
124         * 
125         * @param service
126         * @param request
127         */
128        TransactionHandler( WFService service, Transaction request ) {
129            this.service = service;
130            this.request = request;
131            this.ftMap = service.getMappedFeatureTypes();
132        }
133    
134        /**
135         * Performs the associated transaction.
136         * 
137         * @return transaction response
138         * @throws OGCWebServiceException
139         *             if an error occurred
140         */
141        synchronized TransactionResponse handleRequest()
142                                throws OGCWebServiceException {
143    
144            changedFIDs.clear();
145    
146            validate( this.request );
147    
148            TransactionResponse response = null;
149    
150            acquireDSTransactions();
151    
152            try {
153                try {
154                    response = performOperations();
155                } catch ( OGCWebServiceException e ) {
156                    abortDSTransactions();
157                    throw e;
158                }
159                commitDSTransactions();
160                if ( request.getLockId() != null && request.getReleaseAction() == ALL ) {
161                    try {
162                        LockManager.getInstance().releaseLock( request.getLockId() );
163                    } catch ( DatastoreException e ) {
164                        throw new InvalidParameterValueException( e.getMessage() );
165                    }
166                }
167                if ( request.getLockId() != null && request.getReleaseAction() == SOME ) {
168                    try {
169                        LockManager.getInstance().releaseLockPartly( request.getLockId(), changedFIDs );
170                    } catch ( DatastoreException e ) {
171                        throw new InvalidParameterValueException( e.getMessage() );
172                    }
173                }
174            } finally {
175                releaseDSTransactions();
176            }
177    
178            return response;
179        }
180    
181        /**
182         * Validates the feature instances in the given transaction against the WFS' application schemas.
183         * <p>
184         * The feature instances are assigned the corresponding <code>MappedFeatureType</code> in the process.
185         * 
186         * @param request
187         * @throws OGCWebServiceException
188         */
189        private void validate( Transaction request )
190                                throws OGCWebServiceException {
191    
192            List<TransactionOperation> operations = request.getOperations();
193    
194            Iterator<TransactionOperation> iter = operations.iterator();
195            while ( iter.hasNext() ) {
196                TransactionOperation operation = iter.next();
197                if ( operation instanceof Insert ) {
198                    try {
199                        validateInsert( (Insert) operation );
200                    } catch ( DatastoreException e ) {
201                        throw new OGCWebServiceException( this.getClass().getName(), e.getMessage() );
202                    }
203                } else if ( operation instanceof Delete ) {
204                    validateDelete( (Delete) operation );
205                } else if ( operation instanceof Update ) {
206                    validateUpdate( (Update) operation );
207                } else if ( operation instanceof Native ) {
208                    // nothing to do
209                } else {
210                    String msg = "Internal error. Unhandled transaction operation type '" + operation.getClass().getName()
211                                 + "'.";
212                    throw new OGCWebServiceException( this.getClass().getName(), msg );
213                }
214            }
215        }
216    
217        /**
218         * Validates all feature instances in the given insert operation against the WFS' application schemas.
219         * <p>
220         * The feature instances are assigned the corresponding <code>MappedFeatureType</code> in the process and are
221         * disambiguated, so every feature has a unique feature id afterwards.
222         * 
223         * @param operation
224         * @throws OGCWebServiceException
225         * @throws DatastoreException
226         */
227        @SuppressWarnings("unchecked")
228        private void validateInsert( Insert operation )
229                                throws OGCWebServiceException, DatastoreException {
230            FeatureCollection fc = operation.getFeatures();
231            if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
232                try {
233                    GMLFeatureAdapter ada = new GMLFeatureAdapter( false );
234                    GMLFeatureCollectionDocument doc = ada.export( fc );
235                    LOG.logDebugXMLFile( "TransactionHandler_insert_incoming", doc );
236                } catch ( Exception e ) {
237                    LOG.logError( e.getMessage(), e );
238                }
239            }
240            Validator validator = new Validator( (Map) this.service.getMappedFeatureTypes() );
241            for ( int i = 0; i < fc.size(); i++ ) {
242                validator.validate( fc.getFeature( i ) );
243            }
244    
245            // merge all equal and anonymous features (without fid)
246            FeatureDisambiguator merger = new FeatureDisambiguator( fc );
247            if ( operation.getIdGen() == ID_GEN.USE_EXISTING ) {
248                if ( merger.checkForAnonymousFeatures() ) {
249                    String msg = Messages.getMessage( "WFS_INSERT_USE_EXISTING_AND_NO_FID" );
250                    throw new DatastoreException( msg );
251                }
252            }
253            fc = merger.mergeFeatures();
254        }
255    
256        /**
257         * Validates all feature instances in the given insert operation against the WFS' application schemas.
258         * <p>
259         * The feature instances are assigned the corresponding <code>MappedFeatureType</code> in the process.
260         * 
261         * @param operation
262         * @throws OGCWebServiceException
263         */
264        private void validateDelete( Delete operation )
265                                throws OGCWebServiceException {
266            QualifiedName ftName = operation.getTypeName();
267            MappedFeatureType ft = this.ftMap.get( ftName );
268            if ( ft == null ) {
269                String msg = Messages.getMessage( "WFS_DELETE_FEATURE_TYPE_UNKNOWN", ftName );
270                throw new OGCWebServiceException( this.getClass().getName(), msg );
271            }
272            if ( ft.isAbstract() ) {
273                String msg = Messages.getMessage( "WFS_DELETE_FEATURE_TYPE_ABSTRACT", ftName );
274                throw new OGCWebServiceException( this.getClass().getName(), msg );
275            }
276        }
277    
278        /**
279         * Validates any feature instance in the given update operation against the WFS' application schemas.
280         * <p>
281         * Feature instances are assigned the corresponding <code>MappedFeatureType</code> in the process, property names
282         * are normalized and their values are parsed into the respective objects.
283         * 
284         * @param operation
285         *            update operation
286         * @throws OGCWebServiceException
287         */
288        @SuppressWarnings("unchecked")
289        private void validateUpdate( Update operation )
290                                throws OGCWebServiceException {
291    
292            QualifiedName ftName = operation.getTypeName();
293            MappedFeatureType ft = this.ftMap.get( ftName );
294            if ( ft == null ) {
295                String msg = Messages.getMessage( "WFS_UPDATE_FEATURE_TYPE_UNKNOWN", ftName );
296                throw new OGCWebServiceException( this.getClass().getName(), msg );
297            }
298    
299            Feature feature = operation.getFeature();
300            if ( feature != null ) {
301                Validator validator = new Validator( (Map) this.service.getMappedFeatureTypes() );
302                validator.validate( feature );
303            } else {
304                validateProperties( ft, operation );
305            }
306        }
307    
308        /**
309         * Validates the properties and their replacement values that are specified in the given <code>Update</code>
310         * operation.
311         * <p>
312         * Property names are normalized and their values are parsed into the respective objects.
313         * 
314         * @param ft
315         *            feature type
316         * @param operation
317         *            update operation
318         * @throws OGCWebServiceException
319         */
320        private void validateProperties( MappedFeatureType ft, Update operation )
321                                throws OGCWebServiceException {
322    
323            Map<PropertyPath, FeatureProperty> replacementProps = operation.getReplacementProperties();
324            Map<PropertyPath, FeatureProperty> normalizedProps = new HashMap<PropertyPath, FeatureProperty>();
325    
326            for ( PropertyPath path : replacementProps.keySet() ) {
327                FeatureProperty property = replacementProps.get( path );
328                path = PropertyPathResolver.normalizePropertyPath( ft, null, path );
329                validateProperty( ft, path, property );
330                normalizedProps.put( path, property );
331            }
332    
333            // remove all mappings and add normalized ones
334            replacementProps.clear();
335            for ( PropertyPath path : normalizedProps.keySet() ) {
336                replacementProps.put( path, normalizedProps.get( path ) );
337            }
338        }
339    
340        /**
341         * Validates the property name and it's replacement value.
342         * <p>
343         * Values are parsed into the respective objects.
344         * 
345         * @param ft
346         *            feature type
347         * @param path
348         *            property name
349         * @param replacementProperty
350         *            replacement property value (as XML node)
351         * @throws OGCWebServiceException
352         */
353        private void validateProperty( MappedFeatureType ft, PropertyPath path, FeatureProperty replacementProperty )
354                                throws OGCWebServiceException {
355    
356            for ( int i = 0; i < path.getSteps(); i += 2 ) {
357                // check if feature step is valid
358                PropertyPathStep ftStep = path.getStep( i );
359                FeatureType stepFt = this.ftMap.get( ftStep.getPropertyName() );
360                if ( stepFt == null ) {
361                    String msg = Messages.getMessage( "WFS_UPDATE_FEATURE_STEP_UNKNOWN", path, "unknown" );
362                    throw new OGCWebServiceException( this.getClass().getName(), msg );
363                }
364                MappedGMLSchema schema = ft.getGMLSchema();
365                if ( !schema.isValidSubstitution( ft, stepFt ) ) {
366                    String msg = Messages.getMessage( "WFS_UPDATE_FEATURE_STEP_INVALID", path, stepFt.getName(),
367                                                      ft.getName() );
368                    throw new OGCWebServiceException( this.getClass().getName(), msg );
369                }
370    
371                // check if property step is valid
372                PropertyPathStep propertyStep = path.getStep( i + 1 );
373                QualifiedName propertyName = propertyStep.getPropertyName();
374                PropertyType pt = ft.getProperty( propertyName );
375                if ( pt == null ) {
376                    String msg = Messages.getMessage( "WFS_UPDATE_PROPERTY_STEP_UNKNOWN", path, propertyName, ft.getName() );
377                    throw new OGCWebServiceException( this.getClass().getName(), msg );
378                }
379                if ( i + 2 == path.getSteps() ) {
380                    if ( replacementProperty.getValue() == null && pt.getMinOccurs() > 0 ) {
381                        String msg = Messages.getMessage( "WFS_UPDATE_PROPERTY_NULL_INVALID", path, pt.getMinOccurs() );
382                        throw new InvalidParameterValueException( this.getClass().getName(), msg );
383                    }
384                    if ( replacementProperty.getValue() instanceof Feature ) {
385                        Validator validator = new Validator( (Map) this.service.getMappedFeatureTypes() );
386                        validator.validate( (Feature) replacementProperty.getValue() );
387                    }
388                } else {
389                    if ( !( pt instanceof MappedFeaturePropertyType ) ) {
390                        String msg = Messages.getMessage( "WFS_UPDATE_NOT_FEATURE_PROPERTY", path, propertyName );
391                        throw new OGCWebServiceException( this.getClass().getName(), msg );
392                    }
393                    MappedFeaturePropertyType fpt = (MappedFeaturePropertyType) pt;
394                    ft = fpt.getFeatureTypeReference().getFeatureType();
395                }
396            }
397        }
398    
399        /**
400         * Performs the operations contained in the transaction.
401         * 
402         * @throws OGCWebServiceException
403         */
404        private TransactionResponse performOperations()
405                                throws OGCWebServiceException {
406    
407            int inserts = 0;
408            int deletes = 0;
409            int updates = 0;
410    
411            List<InsertResults> insertResults = new ArrayList<InsertResults>();
412            List<TransactionOperation> operations = request.getOperations();
413            List<Exception> exceptions = new ArrayList<Exception>();
414    
415            Iterator<TransactionOperation> iter = operations.iterator();
416            while ( iter.hasNext() ) {
417                TransactionOperation operation = iter.next();
418                String handle = operation.getHandle();
419                try {
420                    if ( operation instanceof Insert ) {
421                        List<FeatureId> insertedFIDs = performInsert( (Insert) operation );
422                        inserts += insertedFIDs.size();
423    
424                        for ( FeatureId id : insertedFIDs ) {
425                            InsertResults results = new InsertResults( handle, singletonList( id ) );
426                            insertResults.add( results );
427                        }
428                    } else if ( operation instanceof Delete ) {
429                        // the intended behavior is different between versions here, so in #performDelete the exception
430                        // will be thrown now, and caught here if 1.1.0
431                        if ( request.getVersion().equals( "1.0.0" ) ) {
432                            deletes += performDelete( (Delete) operation );
433                        } else {
434                            try {
435                                deletes += performDelete( (Delete) operation );
436                            } catch ( MissingLockIdException e ) {
437                                throw new MissingParameterValueException( "LockId", e.getMessage() );
438                            }
439                        }
440                    } else if ( operation instanceof Update ) {
441                        if ( request.getVersion().equals( "1.0.0" ) ) {
442                            try {
443                                updates += performUpdate( (Update) operation );
444                            } catch ( MissingLockIdException e ) {
445                                exceptions.add( e );
446                            }
447                        } else {
448                            updates += performUpdate( (Update) operation );
449                        }
450                    } else if ( operation instanceof Native ) {
451                        String msg = Messages.getMessage( "WFS_NATIVE_OPERATIONS_UNSUPPORTED" );
452                        throw new OGCWebServiceException( this.getClass().getName(), msg );
453                    } else {
454                        String opType = operation.getClass().getName();
455                        String msg = Messages.getMessage( "WFS_UNHANDLED_OPERATION_TYPE", opType );
456                        throw new OGCWebServiceException( this.getClass().getName(), msg );
457                    }
458                } catch ( DatastoreException e ) {
459                    LOG.logError( e.getMessage(), e );
460                    String msg = "A datastore exception occured during the processing of operation with handle '" + handle
461                                 + "': " + e.getMessage();
462                    throw new InvalidParameterValueException( this.getClass().getName(), msg );
463                }
464            }
465            TransactionResponse response = new TransactionResponse( request, inserts, updates, deletes, insertResults,
466                                                                    exceptions );
467            return response;
468        }
469    
470        /**
471         * Performs the given insert operation.
472         * 
473         * @param insert
474         *            insert operation to be performed
475         * @throws DatastoreException
476         */
477        private List<FeatureId> performInsert( Insert insert )
478                                throws DatastoreException {
479    
480            List<FeatureId> fids = new ArrayList<FeatureId>();
481            FeatureCollection fc = insert.getFeatures();
482    
483            // clear reference to feature collection
484            // insert.setFeatureCollection( null );
485    
486            if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
487                try {
488                    GMLFeatureAdapter ada = new GMLFeatureAdapter( false );
489                    GMLFeatureCollectionDocument doc = ada.export( fc );
490                    LOG.logDebugXMLFile( "TransactionHandler_insert_merged", doc );
491                } catch ( Exception e ) {
492                    LOG.logError( e.getMessage(), e );
493                }
494            }
495    
496            Map<DatastoreTransaction, List<Feature>> taFeaturesMap = new HashMap<DatastoreTransaction, List<Feature>>();
497            FeatureIdAssigner fidAssigner = new FeatureIdAssigner( insert.getIdGen() );
498    
499            // assign features to corresponding datastore transactions
500            for ( int i = 0; i < fc.size(); i++ ) {
501                Feature feature = fc.getFeature( i );
502                QualifiedName ftName = feature.getName();
503                DatastoreTransaction dsTa = this.taMap.get( ftName );
504                // reassign feature ids (if necessary)
505                fidAssigner.assignFID( feature, dsTa );
506                List<Feature> features = taFeaturesMap.get( dsTa );
507                if ( features == null ) {
508                    features = new ArrayList<Feature>();
509                    taFeaturesMap.put( dsTa, features );
510                }
511                features.add( feature );
512            }
513    
514            // TODO remove this hack
515            fidAssigner.markStoredFeatures();
516    
517            // clear reference to fidAssigner (implicitly to feature collection)
518            fidAssigner = null;
519    
520            if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
521                try {
522                    GMLFeatureAdapter ada = new GMLFeatureAdapter( false );
523                    GMLFeatureCollectionDocument doc = ada.export( fc );
524                    LOG.logDebugXMLFile( "TransactionHandler_insert_marked", doc );
525                } catch ( Exception e ) {
526                    LOG.logError( e.getMessage(), e );
527                }
528            }
529    
530            // clear reference to feature collection
531            fc = null;
532    
533            for ( DatastoreTransaction ta : taFeaturesMap.keySet() ) {
534                List<Feature> features = taFeaturesMap.get( ta );
535                fids.addAll( ta.performInsert( features ) );
536            }
537            return fids;
538        }
539    
540        /**
541         * Performs the given delete operation.
542         * 
543         * @param delete
544         *            delete operation to be performed
545         * @throws DatastoreException
546         */
547        private int performDelete( Delete delete )
548                                throws DatastoreException {
549            QualifiedName ftName = delete.getTypeName();
550            MappedFeatureType ft = this.ftMap.get( ftName );
551            DatastoreTransaction dsTa = this.taMap.get( ftName );
552            int deleted = dsTa.performDelete( ft, delete.getFilter(), this.request.getLockId() );
553            return deleted;
554        }
555    
556        /**
557         * Performs the given update operation.
558         * <p>
559         * Assigning of FIDs to replacement features is performed in the {@link DatastoreTransaction}.
560         * 
561         * @param update
562         *            update operation to be perform
563         * @throws DatastoreException
564         */
565        private int performUpdate( Update update )
566                                throws DatastoreException {
567    
568            QualifiedName ftName = update.getTypeName();
569            MappedFeatureType ft = this.ftMap.get( ftName );
570            DatastoreTransaction dsTa = this.taMap.get( ftName );
571            int updated = 0;
572            if ( update.getFeature() == null ) {
573                updated = dsTa.performUpdate( ft, update.getReplacementProperties(), update.getFilter(),
574                                              this.request.getLockId() );
575            } else {
576                updated = dsTa.performUpdate( ft, update.getFeature(), update.getFilter(), this.request.getLockId() );
577            }
578    
579            // to work around API changes...
580            if ( dsTa instanceof SQLTransaction ) {
581                changedFIDs.addAll( ( (SQLTransaction) dsTa ).determineAffectedFIDs( ft, update.getFilter() ) );
582            }
583    
584            return updated;
585        }
586    
587        /**
588         * Acquires the necessary <code>DatastoreTransaction</code>s. For each participating <code>Datastore</code>, one
589         * transaction is needed.
590         * <p>
591         * Fills the <code>taMap</code> and <code>dsToTaMap</code> members of this class.
592         * 
593         * @throws OGCWebServiceException
594         *             if a feature type is unknown or a DatastoreTransaction could not be acquired
595         */
596        private void acquireDSTransactions()
597                                throws OGCWebServiceException {
598            Set<QualifiedName> ftNames = this.request.getAffectedFeatureTypes();
599            for ( QualifiedName ftName : ftNames ) {
600                MappedFeatureType ft = this.ftMap.get( ftName );
601                if ( ft == null ) {
602                    String msg = "FeatureType '" + ftName + "' is not known to the WFS.";
603                    throw new OGCWebServiceException( this.getClass().getName(), msg );
604                }
605                Datastore ds = ft.getGMLSchema().getDatastore();
606                DatastoreTransaction dsTa = this.dsToTaMap.get( ds );
607                if ( dsTa == null ) {
608                    try {
609                        dsTa = ds.acquireTransaction();
610                    } catch ( DatastoreException e ) {
611                        LOG.logError( e.getMessage(), e );
612                        String msg = "Could not acquire transaction for FeatureType '" + ftName + "'.";
613                        throw new OGCWebServiceException( this.getClass().getName(), msg );
614                    }
615                    this.dsToTaMap.put( ds, dsTa );
616                }
617                this.taMap.put( ftName, dsTa );
618            }
619        }
620    
621        /**
622         * Releases all acquired <code>DatastoreTransaction</code>s.
623         * 
624         * @throws OGCWebServiceException
625         *             if a DatastoreTransaction could not be released
626         */
627        private void releaseDSTransactions()
628                                throws OGCWebServiceException {
629            String msg = "";
630            for ( DatastoreTransaction dsTa : this.dsToTaMap.values() ) {
631                LOG.logDebug( "Releasing DatastoreTransaction " + dsTa );
632                try {
633                    dsTa.release();
634                } catch ( DatastoreException e ) {
635                    LOG.logError( "Error releasing DatastoreTransaction: " + e.getMessage(), e );
636                    msg += e.getMessage() + "\n";
637                }
638            }
639            if ( msg.length() != 0 ) {
640                msg = "Could not release one or more DatastoreTransactions: " + msg;
641                throw new OGCWebServiceException( this.getClass().getName(), msg );
642            }
643        }
644    
645        /**
646         * Commits all pending <code>DatastoreTransaction</code>s.
647         * 
648         * @throws OGCWebServiceException
649         *             if a DatastoreException could not be committed
650         */
651        private void commitDSTransactions()
652                                throws OGCWebServiceException {
653            String msg = "";
654            for ( DatastoreTransaction dsTa : this.dsToTaMap.values() ) {
655                LOG.logDebug( "Committing DatastoreTransaction " + dsTa );
656                try {
657                    dsTa.commit();
658                } catch ( DatastoreException e ) {
659                    LOG.logError( "Error committing DatastoreTransaction: " + e.getMessage(), e );
660                    msg += e.getMessage() + "\n";
661                }
662            }
663            if ( msg.length() != 0 ) {
664                msg = "Could not commit one or more DatastoreTransactions: " + msg;
665                throw new OGCWebServiceException( this.getClass().getName(), msg );
666            }
667        }
668    
669        /**
670         * Aborts all pending <code>DatastoreTransaction</code>s.
671         * 
672         * @throws OGCWebServiceException
673         *             if a DatastoreException could not be aborted
674         */
675        private void abortDSTransactions()
676                                throws OGCWebServiceException {
677            String msg = "";
678            for ( DatastoreTransaction dsTa : this.dsToTaMap.values() ) {
679                LOG.logDebug( "Aborting DatastoreTransaction " + dsTa );
680                try {
681                    dsTa.rollback();
682                } catch ( DatastoreException e ) {
683                    LOG.logError( "Error aborting DatastoreTransaction: " + e.getMessage(), e );
684                    msg += e.getMessage() + "\n";
685                }
686            }
687            if ( msg.length() != 0 ) {
688                msg = "Could not abort one or more DatastoreTransactions: " + msg;
689                throw new OGCWebServiceException( this.getClass().getName(), msg );
690            }
691        }
692    }