001: /* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved.
002: * This code is licensed under the GPL 2.0 license, availible at the root
003: * application directory.
004: */
005: package org.geoserver.wfs;
006:
007: import net.opengis.wfs.ActionType;
008: import net.opengis.wfs.AllSomeType;
009: import net.opengis.wfs.InsertedFeatureType;
010: import net.opengis.wfs.TransactionResponseType;
011: import net.opengis.wfs.TransactionType;
012: import net.opengis.wfs.WfsFactory;
013:
014: import org.acegisecurity.Authentication;
015: import org.acegisecurity.context.SecurityContextHolder;
016: import org.acegisecurity.userdetails.UserDetails;
017: import org.eclipse.emf.ecore.EObject;
018: import org.eclipse.emf.ecore.util.FeatureMap;
019: import org.geoserver.platform.GeoServerExtensions;
020: import org.geoserver.platform.ServiceException;
021: import org.geotools.data.DefaultTransaction;
022: import org.geotools.data.FeatureSource;
023: import org.geotools.data.FeatureStore;
024: import org.geotools.xml.EMFUtils;
025: import org.opengis.filter.FilterFactory;
026: import org.springframework.context.ApplicationContext;
027: import org.vfny.geoserver.global.Data;
028: import org.vfny.geoserver.global.FeatureTypeInfo;
029: import java.io.IOException;
030: import java.math.BigInteger;
031: import java.util.ArrayList;
032: import java.util.Collections;
033: import java.util.Comparator;
034: import java.util.HashMap;
035: import java.util.Iterator;
036: import java.util.LinkedHashMap;
037: import java.util.List;
038: import java.util.Map;
039: import java.util.logging.Level;
040: import java.util.logging.Logger;
041: import javax.xml.namespace.QName;
042:
043: /**
044: * Web Feature Service Transaction operation.
045: *
046: * @author Justin Deoliveira, The Open Planning Project
047: *
048: */
049: public class Transaction {
050: /**
051: * logger
052: */
053: static Logger LOGGER = org.geotools.util.logging.Logging
054: .getLogger("org.geoserver.wfs");
055:
056: /**
057: * WFS configuration
058: */
059: protected WFS wfs;
060:
061: /**
062: * The catalog
063: */
064: protected Data catalog;
065:
066: /**
067: * Filter factory
068: */
069: protected FilterFactory filterFactory;
070:
071: /** Geotools2 transaction used for this opperations */
072: protected org.geotools.data.Transaction transaction;
073: protected List transactionElementHandlers = new ArrayList();
074: protected List transactionListeners = new ArrayList();
075: protected List transactionPlugins = new ArrayList();
076:
077: public Transaction(WFS wfs, Data catalog, ApplicationContext context) {
078: this .wfs = wfs;
079: this .catalog = catalog;
080: // register element handlers, listeners and plugins
081: transactionElementHandlers.addAll(GeoServerExtensions
082: .extensions(TransactionElementHandler.class));
083: transactionListeners.addAll(GeoServerExtensions
084: .extensions(TransactionListener.class));
085: transactionPlugins.addAll(GeoServerExtensions
086: .extensions(TransactionPlugin.class));
087: // plugins are listeners too, but I want to make sure they are notified
088: // of
089: // changes in the same order as the other plugin callbacks
090: transactionListeners.removeAll(transactionPlugins);
091: // sort plugins according to priority
092: Collections.sort(transactionPlugins,
093: new TransactionPluginComparator());
094: }
095:
096: public void setFilterFactory(FilterFactory filterFactory) {
097: this .filterFactory = filterFactory;
098: }
099:
100: public TransactionResponseType transaction(TransactionType request)
101: throws WFSException {
102: // make sure server is supporting transactions
103: if ((wfs.getServiceLevel() & WFS.TRANSACTIONAL) == 0) {
104: throw new WFSException("Transaction support is not enabled");
105: }
106:
107: try {
108: return execute(request);
109: } catch (WFSException e) {
110: abort(request); // release any locks
111: throw e;
112: } catch (Exception e) {
113: abort(request); // release any locks
114: throw new WFSException(e);
115: }
116: }
117:
118: /**
119: * Execute Transaction request.
120: *
121: * <p>
122: * The results of this opperation are stored for use by writeTo:
123: *
124: * <ul>
125: * <li> transaction: used by abort & writeTo to commit/rollback </li>
126: * <li> request: used for users getHandle information to report errors </li>
127: * <li> stores: FeatureStores required for Transaction </li>
128: * <li> failures: List of failures produced </li>
129: * </ul>
130: * </p>
131: *
132: * <p>
133: * Because we are using geotools2 locking facilities our modification will
134: * simply fail with IOException if we have not provided proper
135: * authorization.
136: * </p>
137: *
138: * <p>
139: * The specification allows a WFS to implement PARTIAL sucess if it is
140: * unable to rollback all the requested changes. This implementation is able
141: * to offer full Rollback support and will not require the use of PARTIAL
142: * success.
143: * </p>
144: *
145: * @param transactionRequest
146: *
147: * @throws ServiceException
148: * DOCUMENT ME!
149: * @throws WfsException
150: * @throws WfsTransactionException
151: * DOCUMENT ME!
152: */
153: protected TransactionResponseType execute(TransactionType request)
154: throws Exception {
155: // some defaults
156: if (request.getReleaseAction() == null) {
157: request.setReleaseAction(AllSomeType.ALL_LITERAL);
158: }
159:
160: // inform plugins we're about to start, and let them eventually
161: // alter the request
162: for (Iterator it = transactionPlugins.iterator(); it.hasNext();) {
163: TransactionPlugin tp = (TransactionPlugin) it.next();
164: tp.beforeTransaction(request);
165: }
166:
167: // setup the transaction listener multiplexer
168: TransactionListenerMux multiplexer = new TransactionListenerMux();
169:
170: // the geotools transaction
171: transaction = getDatastoreTransaction(request);
172:
173: //
174: // We are going to preprocess our elements,
175: // gathering all the FeatureSources we need
176: //
177: // Map of required FeatureStores by typeName
178: Map stores = new HashMap();
179:
180: // Map of required FeatureStores by typeRef (dataStoreId:typeName)
181: // (This will be added to the contents are harmed)
182: Map stores2 = new HashMap();
183:
184: // List of type names, maintain this list because of the insert hack
185: // described below
186: // List typeNames = new ArrayList();
187: Map elementHandlers = gatherElementHandlers(request.getGroup());
188:
189: // Gather feature types required by transaction elements and validate
190: // the elements
191: // finally gather FeatureStores required by Transaction Elements
192: // and configure them with our transaction
193: //
194: // (I am using element rather than transaction sub request
195: // to agree with the spec docs)
196: for (Iterator it = elementHandlers.entrySet().iterator(); it
197: .hasNext();) {
198: Map.Entry entry = (Map.Entry) it.next();
199: EObject element = (EObject) entry.getKey();
200: TransactionElementHandler handler = (TransactionElementHandler) entry
201: .getValue();
202: Map featureTypeInfos = new HashMap();
203:
204: QName[] typeNames = handler.getTypeNames(element);
205:
206: for (int i = 0; i < typeNames.length; i++) {
207: final QName typeName = typeNames[i];
208: final String name = typeName.getLocalPart();
209: final String namespaceURI;
210:
211: if (typeName.getNamespaceURI() != null) {
212: namespaceURI = typeName.getNamespaceURI();
213: } else {
214: namespaceURI = catalog.getDefaultNameSpace()
215: .getURI();
216: }
217:
218: LOGGER.fine("Locating FeatureSource uri:'"
219: + namespaceURI + "' name:'" + name + "'");
220:
221: final FeatureTypeInfo meta = catalog
222: .getFeatureTypeInfo(name, namespaceURI);
223:
224: if (meta == null) {
225: String msg = "Feature type '" + name
226: + "' is not available: ";
227: String handle = (String) EMFUtils.get(element,
228: "handle");
229: throw new WFSTransactionException(msg,
230: (String) null, handle);
231: }
232:
233: featureTypeInfos.put(typeName, meta);
234: }
235:
236: // check element validity
237: handler.checkValidity(element, featureTypeInfos);
238:
239: // go through all feature type infos data objects, and load feature
240: // stores
241: for (Iterator m = featureTypeInfos.values().iterator(); m
242: .hasNext();) {
243: FeatureTypeInfo meta = (FeatureTypeInfo) m.next();
244: String typeRef = meta.getDataStoreInfo().getId() + ":"
245: + meta.getTypeName();
246:
247: String URI = meta.getNameSpace().getURI();
248: QName elementName = new QName(URI, meta.getTypeName(),
249: meta.getNameSpace().getPrefix());
250: QName elementNameDefault = null;
251:
252: if (catalog.getDefaultNameSpace().getURI().equals(URI)) {
253: elementNameDefault = new QName(meta.getTypeName());
254: }
255:
256: LOGGER.fine("located FeatureType w/ typeRef '"
257: + typeRef + "' and elementName '" + elementName
258: + "'");
259:
260: if (stores.containsKey(elementName)) {
261: // typeName already loaded
262: continue;
263: }
264:
265: try {
266: FeatureSource source = meta.getFeatureSource();
267:
268: if (source instanceof FeatureStore) {
269: FeatureStore store = (FeatureStore) source;
270: store.setTransaction(transaction);
271: stores.put(elementName, source);
272:
273: if (elementNameDefault != null) {
274: stores.put(elementNameDefault, source);
275: }
276:
277: stores2.put(typeRef, source);
278: } else {
279: String msg = elementName + " is read-only";
280: String handle = (String) EMFUtils.get(element,
281: "handle");
282:
283: throw new WFSTransactionException(msg,
284: (String) null, handle);
285: }
286: } catch (IOException ioException) {
287: String msg = elementName + " is not available: "
288: + ioException.getLocalizedMessage();
289: String handle = (String) EMFUtils.get(element,
290: "handle");
291: throw new WFSTransactionException(msg, ioException,
292: handle);
293: }
294: }
295: }
296:
297: // provide authorization for transaction
298: //
299: String authorizationID = request.getLockId();
300:
301: if (authorizationID != null) {
302: if ((wfs.getServiceLevel() & WFS.SERVICE_LOCKING) == 0) {
303: throw new WFSException("Lock support is not enabled");
304: }
305:
306: LOGGER.finer("got lockId: " + authorizationID);
307:
308: if (!lockExists(authorizationID)) {
309: String mesg = "Attempting to use a lockID that does not exist"
310: + ", it has either expired or was entered wrong.";
311: throw new WFSException(mesg, "InvalidParameterValue");
312: }
313:
314: try {
315: transaction.addAuthorization(authorizationID);
316: } catch (IOException ioException) {
317: // This is a real failure - not associated with a element
318: //
319: throw new WFSException("Authorization ID '"
320: + authorizationID + "' not useable",
321: ioException);
322: }
323: }
324:
325: // result
326: TransactionResponseType result = WfsFactory.eINSTANCE
327: .createTransactionResponseType();
328: result.setTransactionResults(WfsFactory.eINSTANCE
329: .createTransactionResultsType());
330: result.getTransactionResults().setHandle(request.getHandle());
331: result.setTransactionSummary(WfsFactory.eINSTANCE
332: .createTransactionSummaryType());
333: result.getTransactionSummary().setTotalInserted(
334: BigInteger.valueOf(0));
335: result.getTransactionSummary().setTotalUpdated(
336: BigInteger.valueOf(0));
337: result.getTransactionSummary().setTotalDeleted(
338: BigInteger.valueOf(0));
339:
340: result.setInsertResults(WfsFactory.eINSTANCE
341: .createInsertResultsType());
342:
343: // execute elements in order, recording results as we go
344: // I will need to record the damaged area for pre commit validation
345: // checks
346: // Envelope envelope = new Envelope();
347: boolean exception = false;
348:
349: try {
350: for (Iterator it = elementHandlers.entrySet().iterator(); it
351: .hasNext();) {
352: Map.Entry entry = (Map.Entry) it.next();
353: EObject element = (EObject) entry.getKey();
354: TransactionElementHandler handler = (TransactionElementHandler) entry
355: .getValue();
356:
357: handler.execute(element, request, stores, result,
358: multiplexer);
359: }
360: } catch (WFSTransactionException e) {
361: exception = true;
362: LOGGER.log(Level.SEVERE, "Transaction failed", e);
363:
364: // transaction failed, rollback
365: ActionType action = WfsFactory.eINSTANCE.createActionType();
366:
367: if (e.getCode() != null) {
368: action.setCode(e.getCode());
369: } else {
370: action.setCode("InvalidParameterValue");
371: }
372:
373: action.setLocator(e.getLocator());
374: action.setMessage(e.getMessage());
375: result.getTransactionResults().getAction().add(action);
376: }
377:
378: // commit
379: boolean committed = false;
380:
381: try {
382: if (exception) {
383: transaction.rollback();
384: } else {
385: // inform plugins we're about to commit
386: for (Iterator it = transactionPlugins.iterator(); it
387: .hasNext();) {
388: TransactionPlugin tp = (TransactionPlugin) it
389: .next();
390: tp.beforeCommit(request);
391: }
392:
393: transaction.commit();
394: committed = true;
395:
396: //
397: // Lets deal with the locks
398: //
399: // Q: Why talk to Data you ask
400: // A: Only class that knows all the DataStores
401: //
402: // We really need to ask all DataStores to release/refresh
403: // because we may have locked Features with this Authorizations
404: // on them, even though we did not refer to them in this
405: // transaction.
406: //
407: // Q: Why here, why now?
408: // A: The opperation was a success, and we have completed the
409: // opperation
410: //
411: // We also need to do this if the opperation is not a success,
412: // you can find this same code in the abort method
413: //
414: if (request.getLockId() != null) {
415: if (request.getReleaseAction() == AllSomeType.ALL_LITERAL) {
416: lockRelease(request.getLockId());
417: } else if (request.getReleaseAction() == AllSomeType.SOME_LITERAL) {
418: lockRefresh(request.getLockId());
419: }
420: }
421: }
422: } finally {
423: transaction.close();
424: transaction = null;
425: }
426:
427: // inform plugins we're done
428: for (Iterator it = transactionPlugins.iterator(); it.hasNext();) {
429: TransactionPlugin tp = (TransactionPlugin) it.next();
430: tp.afterTransaction(request, committed);
431: }
432:
433: //
434: // if ( result.getTransactionResult().getStatus().getPARTIAL() != null )
435: // {
436: // throw new WFSException("Canceling PARTIAL response");
437: // }
438: //
439: // try {
440: // if ( result.getTransactionResult().getStatus().getFAILED() != null )
441: // {
442: // //transaction failed, roll it back
443: // transaction.rollback();
444: // }
445: // else {
446: // transaction.commit();
447: // result.getTransactionResult().getStatus().setSUCCESS(
448: // WfsFactory.eINSTANCE.createEmptyType() );
449: // }
450: //
451: // }
452: // finally {
453: // transaction.close();
454: // transaction = null;
455: // }
456:
457: // JD: this is an issue with the spec, InsertResults must be present,
458: // even if no insert
459: // occured, howwever insert results needs to have at least one
460: // "FeatureId" eliement, sp
461: // we create an FeatureId with an empty fid
462: if (result.getInsertResults().getFeature().isEmpty()) {
463: InsertedFeatureType insertedFeature = WfsFactory.eINSTANCE
464: .createInsertedFeatureType();
465: insertedFeature.getFeatureId().add(
466: filterFactory.featureId("none"));
467:
468: result.getInsertResults().getFeature().add(insertedFeature);
469: }
470:
471: return result;
472:
473: // we will commit in the writeTo method
474: // after user has got the response
475: // response = build;
476: }
477:
478: /**
479: * Looks up the element handlers to be used for each element
480: *
481: * @param group
482: * @return
483: */
484: private Map gatherElementHandlers(FeatureMap group)
485: throws WFSTransactionException {
486: //JD: use a linked hashmap since the order of elements in a transaction
487: // must be respected
488: Map map = new LinkedHashMap();
489:
490: for (Iterator it = group.iterator(); it.hasNext();) {
491: FeatureMap.Entry entry = (FeatureMap.Entry) it.next();
492: EObject element = (EObject) entry.getValue();
493: map.put(element, findElementHandler(element.getClass()));
494: }
495:
496: return map;
497: }
498:
499: /**
500: * Finds the best transaction element handler for the specified element type
501: * (the one matching the most specialized superclass of type)
502: *
503: * @param type
504: * @return
505: */
506: protected final TransactionElementHandler findElementHandler(
507: Class type) throws WFSTransactionException {
508: List matches = new ArrayList();
509:
510: for (Iterator it = transactionElementHandlers.iterator(); it
511: .hasNext();) {
512: TransactionElementHandler handler = (TransactionElementHandler) it
513: .next();
514:
515: if (handler.getElementClass().isAssignableFrom(type)) {
516: matches.add(handler);
517: }
518: }
519:
520: if (matches.isEmpty()) {
521: // try to instantiate one
522: String msg = "No transaction element handler for : ( "
523: + type + " )";
524: throw new WFSTransactionException(msg);
525: }
526:
527: if (matches.size() > 1) {
528: // sort by class hierarchy
529: Comparator comparator = new Comparator() {
530: public int compare(Object o1, Object o2) {
531: TransactionElementHandler h1 = (TransactionElementHandler) o1;
532: TransactionElementHandler h2 = (TransactionElementHandler) o2;
533:
534: if (h2.getElementClass().isAssignableFrom(
535: h1.getElementClass())) {
536: return -1;
537: }
538:
539: return 1;
540: }
541: };
542:
543: Collections.sort(matches, comparator);
544: }
545:
546: return (TransactionElementHandler) matches.get(0);
547: }
548:
549: /**
550: * Creates a gt2 transaction used to execute the transaction call
551: *
552: * @return
553: */
554: protected DefaultTransaction getDatastoreTransaction(
555: TransactionType request) throws IOException {
556: DefaultTransaction transaction = new DefaultTransaction();
557: // use handle as the log messages
558: String username = "anonymous";
559: Authentication authentication = SecurityContextHolder
560: .getContext().getAuthentication();
561: if (authentication != null) {
562: Object principal = authentication.getPrincipal();
563: if (principal instanceof UserDetails) {
564: username = ((UserDetails) principal).getUsername();
565: }
566: }
567:
568: // Ok, this is a hack. We assume there is only one versioning datastore, the postgis one,
569: // and that we can the following properties won't hurt transactio processing anyways...
570: transaction.putProperty("PgVersionedCommitAuthor", username);
571: transaction.putProperty("PgVersionedCommitMessage", request
572: .getHandle());
573:
574: return transaction;
575: }
576:
577: /*
578: * (non-Javadoc)
579: *
580: * @see org.vfny.geoserver.responses.Response#abort()
581: */
582: public void abort(TransactionType request) {
583: if (transaction == null) {
584: return; // no transaction to rollback
585: }
586:
587: try {
588: transaction.rollback();
589: transaction.close();
590: } catch (IOException ioException) {
591: // nothing we can do here
592: LOGGER.log(Level.SEVERE,
593: "Failed trying to rollback a transaction:"
594: + ioException);
595: }
596:
597: if (request.getLockId() != null) {
598: if (request.getReleaseAction() == AllSomeType.SOME_LITERAL) {
599: try {
600: lockRefresh(request.getLockId());
601: } catch (Exception e) {
602: LOGGER.log(Level.WARNING,
603: "Error occured refreshing lock", e);
604: }
605: } else if (request.getReleaseAction() == AllSomeType.ALL_LITERAL) {
606: try {
607: lockRelease(request.getLockId());
608: } catch (Exception e) {
609: LOGGER.log(Level.WARNING,
610: "Error occured releasing lock", e);
611: }
612: }
613: }
614: }
615:
616: void lockRelease(String lockId) throws WFSException {
617: LockFeature lockFeature = new LockFeature(wfs, catalog);
618: lockFeature.release(lockId);
619: }
620:
621: /**
622: * Implement lockExists.
623: *
624: * @param lockID
625: *
626: * @return true if lockID exists
627: *
628: * @see org.geotools.data.Data#lockExists(java.lang.String)
629: */
630: private boolean lockExists(String lockId) throws Exception {
631: LockFeature lockFeature = new LockFeature(wfs, catalog);
632:
633: return lockFeature.exists(lockId);
634: }
635:
636: /**
637: * Refresh lock by authorization
638: *
639: * <p>
640: * Should use your own transaction?
641: * </p>
642: *
643: * @param lockID
644: */
645: private void lockRefresh(String lockId) throws Exception {
646: LockFeature lockFeature = new LockFeature(wfs, catalog);
647: lockFeature.refresh(lockId);
648: }
649:
650: /**
651: * Bounces the single callback we got from transaction event handlers to all
652: * registered listeners
653: *
654: * @author Andrea Aime - TOPP
655: *
656: */
657: private class TransactionListenerMux implements TransactionListener {
658: public void dataStoreChange(List listeners,
659: TransactionEvent event) throws WFSException {
660: for (Iterator it = listeners.iterator(); it.hasNext();) {
661: TransactionListener listener = (TransactionListener) it
662: .next();
663: listener.dataStoreChange(event);
664: }
665: }
666:
667: public void dataStoreChange(TransactionEvent event)
668: throws WFSException {
669: dataStoreChange(transactionPlugins, event);
670: dataStoreChange(transactionListeners, event);
671: }
672: }
673: }
|