/* (c) 2014 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.wfs; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.namespace.QName; import net.opengis.wfs.TransactionResponseType; import net.opengis.wfs.TransactionType; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.FeatureTypeInfo; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.ServiceException; import org.geoserver.wfs.request.TransactionElement; import org.geoserver.wfs.request.TransactionRequest; import org.geoserver.wfs.request.TransactionResponse; import org.geotools.data.DefaultTransaction; import org.geotools.data.FeatureLockException; import org.geotools.data.FeatureSource; import org.geotools.data.FeatureStore; import org.opengis.feature.Feature; import org.opengis.feature.type.FeatureType; import org.opengis.filter.FilterFactory; import org.springframework.context.ApplicationContext; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; /** * Web Feature Service Transaction operation. * * @author Justin Deoliveira, The Open Planning Project * */ public class Transaction { /** * logger */ static Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geoserver.wfs"); /** * WFS configuration */ protected WFSInfo wfs; /** * The catalog */ protected Catalog catalog; /** * Filter factory */ protected FilterFactory filterFactory; /** Geotools2 transaction used for this opperations */ protected org.geotools.data.Transaction transaction; protected List transactionElementHandlers = new ArrayList(); protected List transactionListeners = new ArrayList(); protected List transactionPlugins = new ArrayList(); public Transaction(WFSInfo wfs, Catalog catalog, ApplicationContext context) { this.wfs = wfs; this.catalog = catalog; // register element handlers, listeners and plugins transactionElementHandlers.addAll(GeoServerExtensions.extensions(TransactionElementHandler.class)); transactionListeners.addAll(GeoServerExtensions.extensions(TransactionListener.class)); transactionPlugins.addAll(GeoServerExtensions.extensions(TransactionPlugin.class)); // plugins are listeners too, but I want to make sure they are notified // of // changes in the same order as the other plugin callbacks transactionListeners.removeAll(transactionPlugins); // sort plugins according to priority Collections.sort(transactionPlugins, new TransactionPluginComparator()); } public void setFilterFactory(FilterFactory filterFactory) { this.filterFactory = filterFactory; } public TransactionResponse transaction(TransactionRequest request) throws WFSException { // make sure server is supporting transactions if (!wfs.getServiceLevel().contains(WFSInfo.ServiceLevel.TRANSACTIONAL) ) { throw new WFSException(request, "Transaction support is not enabled"); } try { return execute(request); } catch (WFSException e) { abort(request); // release any locks throw e; } catch (Throwable t) { abort(request); // release any locks throw new WFSException(request, t); } } /** * Execute Transaction request. * * <p> * The results of this opperation are stored for use by writeTo: * * <ul> * <li> transaction: used by abort & writeTo to commit/rollback </li> * <li> request: used for users getHandle information to report errors </li> * <li> stores: FeatureStores required for Transaction </li> * <li> failures: List of failures produced </li> * </ul> * </p> * * <p> * Because we are using geotools2 locking facilities our modification will * simply fail with IOException if we have not provided proper * authorization. * </p> * * <p> * The specification allows a WFS to implement PARTIAL sucess if it is * unable to rollback all the requested changes. This implementation is able * to offer full Rollback support and will not require the use of PARTIAL * success. * </p> * * @param transactionRequest * * @throws ServiceException * DOCUMENT ME! * @throws WfsException * @throws WfsTransactionException * DOCUMENT ME! */ protected TransactionResponse execute(TransactionRequest request) throws Exception { // some defaults if (request.getReleaseAction() == null) { request.setReleaseActionAll(); } // inform plugins we're about to start, and let them eventually // alter the request for (Iterator it = transactionPlugins.iterator(); it.hasNext();) { TransactionPlugin tp = (TransactionPlugin) it.next(); fireBeforeTransaction(request, tp); } // setup the transaction listener multiplexer TransactionListenerMux multiplexer = new TransactionListenerMux(); // the geotools transaction transaction = getDatastoreTransaction(request); request.setTransaction(transaction); // // We are going to preprocess our elements, // gathering all the FeatureSources we need // // Map of required FeatureStores by typeName Map stores = new HashMap(); // Map of required FeatureStores by typeRef (dataStoreId:typeName) // (This will be added to the contents are harmed) Map stores2 = new HashMap(); // List of type names, maintain this list because of the insert hack // described below // List typeNames = new ArrayList(); Map elementHandlers = gatherElementHandlers(request); // Gather feature types required by transaction elements and validate // the elements // finally gather FeatureStores required by Transaction Elements // and configure them with our transaction // // (I am using element rather than transaction sub request // to agree with the spec docs) for (Iterator it = elementHandlers.entrySet().iterator(); it.hasNext();) { Map.Entry entry = (Map.Entry) it.next(); TransactionElement element = (TransactionElement) entry.getKey(); TransactionElementHandler handler = (TransactionElementHandler) entry.getValue(); Map featureTypeInfos = new HashMap(); QName[] typeNames = handler.getTypeNames(element); for (int i = 0; i < typeNames.length; i++) { final QName typeName = typeNames[i]; final String name = typeName.getLocalPart(); final String namespaceURI; if (typeName.getNamespaceURI() != null) { namespaceURI = typeName.getNamespaceURI(); } else { namespaceURI = catalog.getDefaultNamespace().getURI(); } LOGGER.fine("Locating FeatureSource uri:'" + namespaceURI + "' name:'" + name + "'"); final FeatureTypeInfo meta = catalog.getFeatureTypeByName(namespaceURI, name); if (meta == null) { String msg = "Feature type '" + name + "' is not available: "; throw new WFSTransactionException(msg, (String) null, element.getHandle()); } featureTypeInfos.put(typeName, meta); } // check element validity handler.checkValidity(element, featureTypeInfos); // go through all feature type infos data objects, and load feature // stores for (Iterator m = featureTypeInfos.values().iterator(); m.hasNext();) { FeatureTypeInfo meta = (FeatureTypeInfo) m.next(); String typeRef = meta.getStore().getName() + ":" + meta.getName(); String URI = meta.getNamespace().getURI(); QName elementName = new QName(URI, meta.getName(), meta.getNamespace().getPrefix()); QName elementNameDefault = null; if (catalog.getDefaultNamespace().getURI().equals(URI)) { elementNameDefault = new QName(meta.getName()); } LOGGER.fine("located FeatureType w/ typeRef '" + typeRef + "' and elementName '" + elementName + "'"); if (stores.containsKey(elementName)) { // typeName already loaded continue; } try { FeatureSource<? extends FeatureType, ? extends Feature> source = meta.getFeatureSource(null,null); if (source instanceof FeatureStore) { FeatureStore<? extends FeatureType, ? extends Feature> store; store = (FeatureStore<? extends FeatureType, ? extends Feature>) source; store.setTransaction(transaction); stores.put(elementName, source); if (elementNameDefault != null) { stores.put(elementNameDefault, source); } stores2.put(typeRef, source); } else { String msg = elementName + " is read-only"; throw new WFSTransactionException(msg, (String) null, element.getHandle()); } } catch (IOException ioException) { String msg = elementName + " is not available: " + ioException.getLocalizedMessage(); throw new WFSTransactionException(msg, ioException, element.getHandle()); } } } // provide authorization for transaction // String authorizationID = request.getLockId(); if (authorizationID != null) { if (!wfs.getServiceLevel().getOps().contains( WFSInfo.Operation.LOCKFEATURE)) { throw new WFSException(request, "Lock support is not enabled"); } LOGGER.finer("got lockId: " + authorizationID); if (!lockExists(authorizationID)) { String mesg = "Attempting to use a lockID that does not exist" + ", it has either expired or was entered wrong."; throw new WFSException(request, mesg, "InvalidParameterValue"); } try { transaction.addAuthorization(authorizationID); } catch (IOException ioException) { // This is a real failure - not associated with a element // throw new WFSException(request, "Authorization ID '" + authorizationID + "' not useable", ioException); } } // result TransactionResponse result = request.createResponse(); result.setHandle(request.getHandle()); // execute elements in order, recording results as we go // I will need to record the damaged area for pre commit validation // checks // Envelope envelope = new Envelope(); Exception exception = null; try { for (Iterator it = elementHandlers.entrySet().iterator(); it.hasNext();) { Map.Entry entry = (Map.Entry) it.next(); TransactionElement element = (TransactionElement) entry.getKey(); TransactionElementHandler handler = (TransactionElementHandler) entry.getValue(); handler.execute(element, request, stores, result, multiplexer); } } catch (WFSTransactionException e) { LOGGER.log(Level.SEVERE, "Transaction failed", e); exception = e; //another wfs 2.0 hack, but in the case no lock is specified in the request and the tx // is trying to update locked features, we need to use the MissingParameterValue if (request.getVersion().startsWith("2") && e.getCause() instanceof FeatureLockException && request.getLockId() == null) { exception = new WFSTransactionException(e.getMessage(), e, "MissingParameterValue"); } result.addAction(e.getCode() != null ? e.getCode() : "InvalidParameterValue", e.getLocator(), e.getMessage()); } // commit boolean committed = false; try { if (exception != null) { transaction.rollback(); } else { // inform plugins we're about to commit for (Iterator it = transactionPlugins.iterator(); it.hasNext();) { TransactionPlugin tp = (TransactionPlugin) it.next(); fireBeforeCommit(request, tp); } transaction.commit(); committed = true; // // Lets deal with the locks // // Q: Why talk to Data you ask // A: Only class that knows all the DataStores // // We really need to ask all DataStores to release/refresh // because we may have locked Features with this Authorizations // on them, even though we did not refer to them in this // transaction. // // Q: Why here, why now? // A: The opperation was a success, and we have completed the // opperation // // We also need to do this if the opperation is not a success, // you can find this same code in the abort method // String lockId = request.getLockId(); if (lockId != null) { if (request.isReleaseActionAll()) { lockRelease(lockId); } else if (request.isReleaseActionSome()) { lockRefresh(lockId); } } } } finally { transaction.close(); transaction = null; request.setTransaction(null); } // inform plugins we're done for (Iterator it = transactionPlugins.iterator(); it.hasNext();) { TransactionPlugin tp = (TransactionPlugin) it.next(); fireAfterTransaction(request, result, committed, tp); } // // if ( result.getTransactionResult().getStatus().getPARTIAL() != null ) // { // throw new WFSException("Canceling PARTIAL response"); // } // // try { // if ( result.getTransactionResult().getStatus().getFAILED() != null ) // { // //transaction failed, roll it back // transaction.rollback(); // } // else { // transaction.commit(); // result.getTransactionResult().getStatus().setSUCCESS( // WfsFactory.eINSTANCE.createEmptyType() ); // } // // } // finally { // transaction.close(); // transaction = null; // } if (exception != null) { //WFS 2.0 wants us to throw the exception if (request.getVersion() != null && request.getVersion().startsWith("2")) { if (!(exception instanceof WFSException && ((WFSException)exception).getCode() != null)) { //wrap to get the default code exception = new WFSException(request, exception); } throw exception; } } // JD: this is an issue with the spec, InsertResults must be present, // even if no insert // occured, howwever insert results needs to have at least one // "FeatureId" eliement, sp // we create an FeatureId with an empty fid List insertedFeatures = result.getInsertedFeatures(); if (insertedFeatures != null && insertedFeatures.isEmpty()) { result.addInsertedFeature(null, filterFactory.featureId("none")); } return result; // we will commit in the writeTo method // after user has got the response // response = build; } void fireAfterTransaction(TransactionRequest request, TransactionResponse result, boolean committed, TransactionPlugin tp) { TransactionType tx = TransactionRequest.WFS11.unadapt(request); TransactionResponseType tr = TransactionResponse.WFS11.unadapt(result); if (tx != null && tr != null) tp.afterTransaction(tx, tr, committed); } void fireBeforeCommit(TransactionRequest request, TransactionPlugin tp) { TransactionType tx = TransactionRequest.WFS11.unadapt(request); if (tx != null) tp.beforeCommit(tx); } void fireBeforeTransaction(TransactionRequest request, TransactionPlugin tp) { TransactionType tx = TransactionRequest.WFS11.unadapt(request); if (tx != null) tp.beforeTransaction(tx); } /** * Looks up the element handlers to be used for each element * * @param group * */ private Map gatherElementHandlers(TransactionRequest request) throws WFSTransactionException { //JD: use a linked hashmap since the order of elements in a transaction // must be respected Map map = new LinkedHashMap(); List<TransactionElement> elements = request.getElements(); for (TransactionElement element : elements) { map.put(element, findElementHandler(element.getClass())); } return map; } /** * Finds the best transaction element handler for the specified element type * (the one matching the most specialized superclass of type) * * @param type * */ protected final TransactionElementHandler findElementHandler(Class type) throws WFSTransactionException { List matches = new ArrayList(); for (Iterator it = transactionElementHandlers.iterator(); it.hasNext();) { TransactionElementHandler handler = (TransactionElementHandler) it.next(); if (handler.getElementClass().isAssignableFrom(type)) { matches.add(handler); } } if (matches.isEmpty()) { // try to instantiate one String msg = "No transaction element handler for : ( " + type + " )"; throw new WFSTransactionException(msg); } if (matches.size() > 1) { // sort by class hierarchy Comparator comparator = new Comparator() { public int compare(Object o1, Object o2) { TransactionElementHandler h1 = (TransactionElementHandler) o1; TransactionElementHandler h2 = (TransactionElementHandler) o2; if (h2.getElementClass().isAssignableFrom(h1.getElementClass())) { return -1; } return 1; } }; Collections.sort(matches, comparator); } return (TransactionElementHandler) matches.get(0); } /** * Creates a gt2 transaction used to execute the transaction call * <p> * request's {@link TransactionRequest#getExtendedProperties() extended properties} are set as * {@link org.geotools.data.Transaction#putProperty(Object, Object) transaction properties} so * that they're available to the lower level API. * <p> * These properties can be provided for example by * {@link TransactionPlugin#beforeTransaction(TransactionType)} implementations. A typical * example is a custom authentication module providing extra user information that upon * transaction commit can be used by versioning geotools datastore to complete the information * required for its records (such as committer full name, email, etc) * * @return a new geotools transaction */ protected DefaultTransaction getDatastoreTransaction(TransactionRequest request) throws IOException { DefaultTransaction transaction = new DefaultTransaction(); // use handle as the log messages String username = "anonymous"; Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if(authentication != null) { Object principal = authentication.getPrincipal(); if(principal instanceof UserDetails) { username = ((UserDetails) principal).getUsername(); } } // Ok, this is a hack. We assume there is only one versioning datastore, the postgis one, // and that we can the following properties won't hurt transactio processing anyways... transaction.putProperty("VersioningCommitAuthor", username); transaction.putProperty("VersioningCommitMessage", request.getHandle()); // transfer any tx extended property down to the geotools transaction. // TransactionPlugins can contribute such info in their beforeTransaction() // implementation Map<?, ?> extendedProperties = request.getExtendedProperties(); if (extendedProperties != null) { for (Entry<?, ?> e : extendedProperties.entrySet()) { Object propKey = e.getKey(); Object propValue = e.getValue(); transaction.putProperty(propKey, propValue); } } return transaction; } /* * (non-Javadoc) * * @see org.vfny.geoserver.responses.Response#abort() */ public void abort(TransactionRequest request) { if (transaction == null) { return; // no transaction to rollback } try { transaction.rollback(); transaction.close(); } catch (IOException ioException) { // nothing we can do here LOGGER.log(Level.SEVERE, "Failed trying to rollback a transaction:" + ioException); } String lockId = request.getLockId(); if (lockId != null) { if (request.isReleaseActionSome()) { try { lockRefresh(lockId); } catch (Exception e) { LOGGER.log(Level.WARNING, "Error occured refreshing lock", e); } } else if (request.isReleaseActionAll()) { try { lockRelease(lockId); } catch (Exception e) { LOGGER.log(Level.WARNING, "Error occured releasing lock", e); } } } } void lockRelease(String lockId) throws WFSException { LockFeature lockFeature = new LockFeature(wfs, catalog); lockFeature.release(lockId); } /** * Implement lockExists. * * @param lockID * * @return true if lockID exists * * @see org.geotools.data.Data#lockExists(java.lang.String) */ private boolean lockExists(String lockId) throws Exception { LockFeature lockFeature = new LockFeature(wfs, catalog); return lockFeature.exists(lockId); } /** * Refresh lock by authorization * * <p> * Should use your own transaction? * </p> * * @param lockID */ private void lockRefresh(String lockId) throws Exception { LockFeature lockFeature = new LockFeature(wfs, catalog); lockFeature.refresh(lockId); } /** * Bounces the single callback we got from transaction event handlers to all * registered listeners * * @author Andrea Aime - TOPP * */ private class TransactionListenerMux implements TransactionListener { public void dataStoreChange(List listeners, TransactionEvent event) throws WFSException { for (Iterator it = listeners.iterator(); it.hasNext();) { TransactionListener listener = (TransactionListener) it.next(); listener.dataStoreChange(event); } } public void dataStoreChange(TransactionEvent event) throws WFSException { dataStoreChange(transactionPlugins, event); dataStoreChange(transactionListeners, event); } } }