/* DBSession.java The GANYMEDE object storage system. Created: 26 August 1996 Module By: Jonathan Abbey, jonabbey@arlut.utexas.edu ----------------------------------------------------------------------- Ganymede Directory Management System Copyright (C) 1996-2014 The University of Texas at Austin Ganymede is a registered trademark of The University of Texas at Austin Contact information Web site: http://www.arlut.utexas.edu/gash2 Author Email: ganymede_author@arlut.utexas.edu Email mailing list: ganymede@arlut.utexas.edu US Mail: Computer Science Division Applied Research Laboratories The University of Texas at Austin PO Box 8029, Austin TX 78713-8029 Telephone: (512) 835-3200 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package arlut.csd.ganymede.server; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Vector; import arlut.csd.Util.RandomUtils; import arlut.csd.Util.TranslationService; import arlut.csd.ganymede.common.Invid; import arlut.csd.ganymede.common.ObjectStatus; import arlut.csd.ganymede.common.QueryDescriber; import arlut.csd.ganymede.common.ReturnVal; import arlut.csd.ganymede.common.SchemaConstants; import arlut.csd.ganymede.rmi.db_field; /*------------------------------------------------------------------------------ class DBSession ------------------------------------------------------------------------------*/ /** * <p>DBSession is the Ganymede server's * {@link arlut.csd.ganymede.server.DBStore DBStore}-level session class. Each * client or server process that interacts with the Ganymede database * must eventually do so through a DBSession object. Clients and * server processes generally interact directly with a * {@link arlut.csd.ganymede.server.GanymedeSession GanymedeSession}, by way of * the {@link arlut.csd.ganymede.rmi.Session Session} interface on the * part of the client. The GanymedeSession talks to the DBSession class * to actually interact with the database.</p> * * <p>Most particularly, DBSession handles transactions and namespace * logic for the Ganymede server, as well as providing the actual * check-out/create/ check-in methods that GanymedeSession calls. * GanymedeSession tends to have the more high-level * application/permissions logic, while DBSession is more concerned * with internal database issues. As well, GanymedeSession is * designed to be directly accessed and manipulated by the client, * while DBSession is accessed only by (presumably trusted) * server-side code, that needs to bypass the security logic in * GanymedeSession.</p> * * <p>The DBSession contains code and logic to actually manipulate the * Ganymede database (the {@link arlut.csd.ganymede.server.DBObjectBase DBObjectBase}, * {@link arlut.csd.ganymede.server.DBObject DBObject}, and * {@link arlut.csd.ganymede.server.DBEditObject DBEditObject} objects held * in the DBStore). The DBSession class connects to the extensive * transaction logic implemented in the {@link arlut.csd.ganymede.server.DBEditSet DBEditSet} * class, as well as the database locking handled by the * {@link arlut.csd.ganymede.server.DBLock DBLock} class.</p> * * @author Jonathan Abbey, jonabbey@arlut.utexas.edu, ARL:UT */ public final class DBSession implements QueryDescriber { static boolean debug = false; public final static void setDebug(boolean val) { debug = val; } /** * <p>TranslationService object for handling string localization in the Ganymede * server.</p> */ static TranslationService ts = TranslationService.getTranslationService("arlut.csd.ganymede.server.DBSession"); // --- /** * <p>User-level session reference. As mentioned above, * {@link arlut.csd.ganymede.server.GanymedeSession GanymedeSession} has the * user-level permissions handling, while DBSession has the database * handling.</p> */ GanymedeSession GSession; /** * <p>Root object of the Ganymede database system.</p> */ DBStore store; /** * <p>Manager for locks held by this session.</p> */ private DBSessionLockManager lockManager; /** * <p>Transaction handle for this session.</p> */ DBEditSet editSet; /** * <p>Identifying key used in the lock system to identify owner of * locks.</p> */ private Object key; /* -- */ /** * <p>Constructor for DBSession.</p> * * <p>The key passed to the DBSession constructor is intended to be used * to allow code to save an identifier in the DBSession.. this might be * a thread object or a higher level session object or whatever. Eventually * I expect I'll replace this generic key with some sort of reporting * Interface object.</p> * * <p>This constructor is intended to be called by the DBStore login() method.</p> * * @param store The DBStore database this session belongs to. * @param GSession The Ganymede session associated with this DBSession * @param key An identifying key with meaning to whatever code is * using arlut.csd.ganymede. Must be unique for DBObjectBase locking. * */ DBSession(DBStore store, GanymedeSession GSession, Object key) { this.store = store; this.key = key; this.GSession = GSession; editSet = null; lockManager = new DBSessionLockManager(this); } /** * <p>Close out this DBSession, aborting any open transaction, and releasing * any held read/write/dump locks.</p> */ public synchronized void logout() { releaseAllLocks(); if (editSet != null) { abortTransaction(); } // help GC store = null; GSession = null; lockManager = null; key = null; } /** * <p>This method is provided so that custom * {@link arlut.csd.ganymede.server.DBEditObject DBEditObject} subclasses * can get access to methods on our DBStore.</p> */ public DBStore getStore() { return store; } /** * <p>Create a new object in the database.</p> * * <p>This method creates a slot in the object base of the proper * object type. The created object is associated with the current * transaction. When the transaction is committed, the created * object will inserted into the database, and will become visible * to other sessions.</p> * * <p>The created object will be given an object id. The {@link * arlut.csd.ganymede.server.DBEditObject DBEditObject} can be * queried to determine its invid.</p> * * <p>The created DBEditObject will have its fields initialized by * the {@link arlut.csd.ganymede.server.DBObjectBase DBObjectBase} * {@link arlut.csd.ganymede.server.DBObjectBase#objectHook * objectHook} custom DBEditObject's {@link * arlut.csd.ganymede.server.DBEditObject#initializeNewObject() * initializeNewObject()} method.</p> * * @param object_type Type of the object to be created * @param chosenSlot Invid to create the new object with. normally * only used in internal Ganymede code in conjunction with the * addition of new kinds of built-in objects during development * @param owners List of Invids for owner group objects to make * initial owners for the newly created object * * @return A ReturnVal that indicates the success or failure of the * object creation, with a db_object reference to the created * DBObject on success. * * @see arlut.csd.ganymede.server.DBStore */ public synchronized ReturnVal createDBObject(short object_type, Invid chosenSlot, List<Invid> owners) { DBObjectBase base; DBEditObject e_object; ReturnVal retVal = null; /* -- */ if (editSet == null) { throw new RuntimeException(ts.l("global.notransaction", "createDBObject")); } base = store.getObjectBase(object_type); // we create the object.. this just gets the DBEditObject // created.. all of its fields will be created, but it won't be // linked into the database or editset or anything else yet. e_object = base.createNewObject(editSet, chosenSlot); // Checkpoint the transaction at this point so that we can // recover if we can't get the object into the owner groups // it needs to go into String ckp_label = RandomUtils.getSaltedString("create[" + base.getName() + "]"); checkpoint(ckp_label); boolean checkpointed = true; try { // set ownership for this new object if it is not an embedded object if (!base.isEmbedded() && (owners != null)) { InvidDBField inf = e_object.getInvidField(SchemaConstants.OwnerListField); /* -- */ for (Invid tmpInvid: owners) { if (tmpInvid.getType() != SchemaConstants.OwnerBase) { // "bad ownership invid" throw new RuntimeException(ts.l("createDBObject.badowner")); } // we don't want to explicitly record supergash ownership if (tmpInvid.getNum() == SchemaConstants.OwnerSupergash) { continue; } retVal = inf.addElementLocal(tmpInvid); if (!ReturnVal.didSucceed(retVal)) { try { DBObject owner = viewDBObject(tmpInvid); String name = owner.getLabel(); String checkedOutBy = owner.getShadow().editset.description; // "Owner group {0} is currently checked out by:\n{1}" retVal.getDialog().appendText("\n" + ts.l("createDBObject.checkedout", name, checkedOutBy)); } catch (NullPointerException ex) { } return retVal; } } } // register the object as created // this can fail if the e_object comes to us already pointing // to an object that is being deleted by another transaction // by way of an asymmetric InvidDBField. This should never // happen, as it would require a custom object's constructor // to have set an InvidDBField value instead of putting that // logic in its initializeNewObject() method, but we should // check just in case. if (!editSet.addObject(e_object)) { // "Couldn''t create object" // "Couldn''t create the object, because it came pre-linked to a deleted object.\n // Don''t worry, this wasn''t your fault.\n // Talk to whoever customized Ganymede for you, or try again later." return Ganymede.createErrorDialog(this.getGSession(), ts.l("createDBObject.failure"), ts.l("createDBObject.addObject_failed")); } // update admin consoles // // Now that we've added our new object to our transaction, we need // to update objects checked-out counts. After this point, doing a // rollback will cause the session and server check-out counts to // be decremented for our new object, and we have to increment it // before that happens. // // we need to do the session's checkout count first, then // update the database's overall checkout, which // will trigger a console update GSession.checkOut(); store.checkOut(); if (!base.isEmbedded()) { // do any work that the custom code for this object wants // to have done // note that we're not doing this for embedded objects, // because we want to defer the initializeNewObject() call // until the embedded object has been linked to its // parent, which is done by // InvidDBField.createNewEmbedded(). retVal = e_object.initializeNewObject(); if (!ReturnVal.didSucceed(retVal)) { return retVal; } } // okay, we're good, and we won't need to revert to the checkpoint. // Clear out the checkpoint and continue popCheckpoint(ckp_label); checkpointed = false; } finally { // just in case we had an exception thrown.. all standard // returns from the above try clause should have taken care of // the checkpoint if (checkpointed) { rollback(ckp_label); } } // set the following false to true to view the initial state of the object if (false) { // "Created new object : {0}, invid = {1}" Ganymede.debug(ts.l("createDBObject.created", e_object.getLabel(), e_object.getInvid().toString())); DBField[] fields = e_object.listDBFields(); for (int i = 0; i < fields.length; i++) { // "field {0} is {1}:{2}" Ganymede.debug(ts.l("createDBObject.field_report", Integer.valueOf(i), Integer.valueOf(fields[i].getID()), fields[i].getName())); } } // finish initialization of the object.. none of this should fail // since we are just setting text and date fields if (!base.isEmbedded()) { DateDBField df; StringDBField sf; Date modDate = new Date(); String result; /* -- */ // set creator info to something non-null df = e_object.getDateField(SchemaConstants.CreationDateField); df.setValueLocal(modDate); sf = e_object.getStringField(SchemaConstants.CreatorField); result = getID(); if (editSet.description != null) { result += ": " + editSet.description; } sf.setValueLocal(result); // set modifier info to something non-null df = e_object.getDateField(SchemaConstants.ModificationDateField); df.setValueLocal(modDate); sf = e_object.getStringField(SchemaConstants.ModifierField); result = getID(); if (editSet.description != null) { result += ": " + editSet.description; } sf.setValueLocal(result); } retVal = new ReturnVal(true); retVal.setObject(e_object); retVal.setInvid(e_object.getInvid()); return retVal; } /** * <p>Create a new object in the database.</p> * * <p>This method creates a slot in the object base of the * proper object type. The created object is associated * with the current transaction. When the transaction * is committed, the created object will inserted into * the database, and will become visible to other * sessions.</p> * * <p>The created object will be given an object id. * The {@link arlut.csd.ganymede.server.DBEditObject DBEditObject} * can be queried to determine its invid.</p> * * <p>The created DBEditObject will have its fields initialized * by the {@link arlut.csd.ganymede.server.DBObjectBase DBObjectBase} * {@link arlut.csd.ganymede.server.DBObjectBase#objectHook objectHook} * custom DBEditObject's * {@link arlut.csd.ganymede.server.DBEditObject#initializeNewObject() initializeNewObject()} * method.</p> * * <p>This method returns a ReturnVal object to convey the * result of the creation. Call the * {@link arlut.csd.ganymede.common.ReturnVal#getObject() getObject()} method on * the returned ReturnVal in order to get the created DBEditObject. Note * that the ReturnVal.getObject() method is intended to support passing * a remote db_object reference to the client, so on the server, it is * necessary to cast the db_object reference to a DBEditObject reference * for use on the server.</p> * * @param object_type Type of the object to be created * @param owners List of Invids for owner group objects to make * initial owners for the newly created object * * @return A ReturnVal that indicates the success or failure of the * object creation, with a db_object reference to the created * DBObject on success. * * @see arlut.csd.ganymede.server.DBStore */ public ReturnVal createDBObject(short object_type, List<Invid> owners) { return createDBObject(object_type, null, owners); } /** * <p>Pull an object out of the database for editing.</p> * * <p>This method is used to check an object out of the database for editing. * Only one session can have a particular object checked out for editing at * a time.</p> * * <p>The session has to have a transaction opened before it can pull * an object out for editing.</p> * * @param invid The invariant id of the object to be modified. * * @return null if the object could not be found for editing * * @see arlut.csd.ganymede.server.DBObjectBase */ public DBEditObject editDBObject(Invid invid) { return editDBObject(invid.getType(), invid.getNum()); } /** * <p>Pull an object out of the database for editing.</p> * * <p>This method is used to check an object out of the database for editing. * Only one session can have a particular object checked out for editing at * a time.</p> * * <p>The session has to have a transaction opened before it can pull a * new object out for editing. If the object specified by <baseID, * objectID> is part of the current transaction, the transactional * copy will be returned, and no readLock is strictly necessary in * that case.</p> * * <p>This method doesn't do permission checking.. that is performed at the * {@link arlut.csd.ganymede.server.GanymedeSession GanymedeSession} level.</p> * * @param baseID The short id number of the * {@link arlut.csd.ganymede.server.DBObjectBase DBObjectBase} containing the object to * be edited. * * @param objectID The int id number of the object to be edited within the specified * object base. * * @return null if the object could not be found for editing */ public synchronized DBEditObject editDBObject(short baseID, int objectID) { DBObject obj; /* -- */ if (editSet == null) { throw new RuntimeException(ts.l("global.notransaction", "editDBObject")); } obj = viewDBObject(baseID, objectID); if (obj == null) { System.err.println(ts.l("editDBObject.noobject", Integer.valueOf(baseID), Integer.valueOf(objectID))); return null; } if (obj instanceof DBEditObject) { // we already have a copy checked out.. go ahead and // return a reference to our copy return (DBEditObject) obj; } else { // the createShadow call will update the check-out counts DBEditObject eObj = obj.createShadow(editSet); // *sync* DBObject return eObj; // if null, GanymedeSession.edit_db_object() will handle the error } } /** * <p>Get a reference to a read-only copy of an object in the * {@link arlut.csd.ganymede.server.DBStore DBStore}.</p> * * <p>If this session has a transaction currently open, this method will return * the checked out shadow of invid, if it has been checked out by this * transaction.</p> * * <p>Note that unless the object has been checked out by the current session, * this method will return access to the object as it is stored directly * in the main datastore hashes. This means that the object will be * read-only and will grant all accesses, as it will have no notion of * what session or transaction owns it. If you need to have access to the * object's fields be protected, use {@link arlut.csd.ganymede.server.GanymedeSession GanymedeSession}'s * {@link arlut.csd.ganymede.server.GanymedeSession#view_db_object(arlut.csd.ganymede.common.Invid) * view_db_object()} method to get the object.</p> * * @param invid The invariant id of the object to be viewed. * @param getOriginal if true, viewDBObject will return the original * version of a DBEditObject in this session if the specified object * is in the middle of being deleted */ public DBObject viewDBObject(Invid invid, boolean getOriginal) { return viewDBObject(invid.getType(), invid.getNum(), getOriginal); } /** * <p>Get a reference to a read-only copy of an object in the * {@link arlut.csd.ganymede.server.DBStore DBStore}.</p> * * <p>If this session has a transaction currently open, this method will return * the checked out shadow of invid, if it has been checked out by this * transaction.</p> * * <p>Note that unless the object has been checked out by the current session, * this method will return access to the object as it is stored directly * in the main datastore hashes. This means that the object will be * read-only and will grant all accesses, as it will have no notion of * what session or transaction owns it. If you need to have access to the * object's fields be protected, use {@link arlut.csd.ganymede.server.GanymedeSession GanymedeSession}'s * {@link arlut.csd.ganymede.server.GanymedeSession#view_db_object(arlut.csd.ganymede.common.Invid) * view_db_object()} method to get the object.</p> * * @param invid The invariant id of the object to be viewed. */ public DBObject viewDBObject(Invid invid) { return viewDBObject(invid.getType(), invid.getNum()); } /** * <p>Get a reference to a read-only copy of an object in the * {@link arlut.csd.ganymede.server.DBStore DBStore}.</p> * * <p>If this session has a transaction currently open, this method will return * the checked out shadow of invid, if it has been checked out by this * transaction.</p> * * <p>Note that unless the object has been checked out by the current session, * this method will return access to the object as it is stored directly * in the main datastore hashes. This means that the object will be * read-only and will grant all accesses, as it will have no notion of * what session or transaction owns it. If you need to have access to the * object's fields be protected, use {@link arlut.csd.ganymede.server.GanymedeSession GanymedeSession}'s * {@link arlut.csd.ganymede.server.GanymedeSession#view_db_object(arlut.csd.ganymede.common.Invid) * view_db_object()} method to get the object.</p> * * @param baseID The short id number of the DBObjectBase containing the object to * be viewed. * * @param objectID The int id number of the object to be viewed within the specified * object base. */ public DBObject viewDBObject(short baseID, int objectID) { return viewDBObject(baseID, objectID, false); } /** * <p>Get a reference to a read-only copy of an object in the * {@link arlut.csd.ganymede.server.DBStore DBStore}.</p> * * <p>If this session has a transaction currently open, this method will return * the checked out shadow of invid, if it has been checked out by this * transaction.</p> * * <p>Note that unless the object has been checked out by the current session, * this method will return access to the object as it is stored directly * in the main datastore hashes. This means that the object will be * read-only and will grant all accesses, as it will have no notion of * what session or transaction owns it. If you need to have access to the * object's fields be protected, use {@link arlut.csd.ganymede.server.GanymedeSession GanymedeSession}'s * {@link arlut.csd.ganymede.server.GanymedeSession#view_db_object(arlut.csd.ganymede.common.Invid) * view_db_object()} method to get the object.</p> * * @param baseID The short id number of the DBObjectBase containing the object to * be viewed. * * @param objectID The int id number of the object to be viewed within the specified * object base. * * @param getOriginal if true, viewDBObject will return the original * version of a DBEditObject in this session if the specified object * is in the middle of being deleted */ public DBObject viewDBObject(short baseID, int objectID, boolean getOriginal) { DBObjectBase base; DBObject obj = null; /* -- */ base = Ganymede.db.getObjectBase(baseID); if (base == null) { return null; } // this should be safe, as there shouldn't be any threads doing a // viewDBObject while the schema is being edited, by virtue of the // loginSemaphore.. otherwise, something wacky might happen, like // the DBObjectBase being broken down and having the objectTable // field set to null. // We use the DBObjectTable's synchronized get() method so that we // can look up objects even while the DBObjectBase is locked // during another transaction's commit obj = base.getObject(objectID); // if we aren't editing anything, we can't possibly have our own // version of the object checked out if (!isTransactionOpen()) { return obj; } // if we are editing something, we need to be more careful about // synchronization with editing methods synchronized (this) { if (obj == null) { // not in the persistent store.. maybe we created it in // this transaction, or maybe it just doesn't exist. return editSet.findObject(Invid.createInvid(baseID, objectID)); } // okay, we found it and we've got a transaction open.. see if the // object is being edited and, if so, if it is us that is doing it DBEditObject shadow = obj.getShadow(); if (shadow == null || shadow.getDBSession() != this) { return obj; } // okay, the object is being edited by us.. if we are supposed to // return the original version of an object being deleted, and // this one is, return the original if (getOriginal && shadow.getStatus() == ObjectStatus.DELETING) { return obj; } // else return the object being edited return shadow; } } /** * <p>Remove an object from the database</p> * * <p>This method method can only be called in the context of an open * transaction. This method will mark an object for deletion. When * the transaction is committed, the object is removed from the * database. If the transaction is aborted, the object remains in * the database unchanged.</p> * * @param invid Invid of the object to be deleted */ public ReturnVal deleteDBObject(Invid invid) { return deleteDBObject(invid.getType(), invid.getNum()); // *sync* } /** * <p>Remove an object from the database</p> * * <p>This method method can only be called in the context of an open * transaction. This method will check an object out of the * {@link arlut.csd.ganymede.server.DBStore DBStore} * and add it to the editset's deletion list. When the transaction * is committed, the object has its remove() method called to do * cleanup, and the editSet nulls the object's slot in the DBStore. * If the transaction is aborted, the object remains in the database * unchanged.</p> * * @param baseID id of the object base containing the object to be deleted * @param objectID id of the object to be deleted * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public synchronized ReturnVal deleteDBObject(short baseID, int objectID) { DBObject obj; DBEditObject eObj; /* -- */ if (editSet == null) { throw new RuntimeException(ts.l("global.notransaction", "deleteDBObject")); } obj = viewDBObject(baseID, objectID); // we have to have an editable object in order to delete it.. see // if we can do that if (obj instanceof DBEditObject) { eObj = (DBEditObject) obj; } else { eObj = obj.createShadow(editSet); } if (eObj == null) { return Ganymede.createErrorDialog(this.getGSession(), ts.l("deleteDBObject.cant_delete", obj.getLabel()), ts.l("deleteDBObject.cant_delete_text", obj.getLabel())); } return deleteDBObject(eObj); } /** * <p>Remove an object from the database</p> * * <p>This method method can only be called in the context of an open * transaction. Because the object must be checked out (which is the * only way to obtain a {@link arlut.csd.ganymede.server.DBEditObject DBEditObject}), * no other locking is * required. This method will take an object out of the * {@link arlut.csd.ganymede.server.DBStore DBStore}, do * whatever immediate removal logic is required, and mark it as * deleted in the transaction. When the transaction is committed, * the object will be expunged from the database.</p> * * <p>Note that this method does not check to see whether permission * has been obtained to delete the object.. that's done in * {@link arlut.csd.ganymede.server.GanymedeSession GanymedeSession}'s * {@link arlut.csd.ganymede.server.GanymedeSession#remove_db_object(arlut.csd.ganymede.common.Invid) remove_db_object()} * method.</p> * * @param eObj An object checked out in the current transaction to be deleted * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public synchronized ReturnVal deleteDBObject(DBEditObject eObj) { ReturnVal retVal, retVal2; String ckp_label; /* -- */ if (editSet == null) { throw new RuntimeException(ts.l("global.notransaction", "deleteDBObject")); } ckp_label = RandomUtils.getSaltedString("del[" + eObj.getLabel() + "]"); switch (eObj.getStatus()) { case DBEditObject.CREATING: case DBEditObject.EDITING: // we have to checkpoint before we set the status to delete, // or else a later rollback will still leave the object in // must-delete status if the transaction is committed checkpoint(ckp_label); // by calling the synchronized setDeleteStatus() method on the // DBDeletionManager, we announce our intention to delete this // object, and lock out any other objects from establishing // asymmetrical links to this object.. if this fails, another // object in an open transaction has linked to this eObj // without having checked it out for editing (which always // means an asymmetrical link), and we can't let the object be // deleted if (!DBDeletionManager.setDeleteStatus(eObj, this)) { // if setDeleteStatus() fails, nothing will have been changed, // so we can just pop our checkpoint popCheckpoint(ckp_label); return Ganymede.createErrorDialog(this.getGSession(), ts.l("deleteDBObject.cant_delete", eObj.toString()), ts.l("deleteDBObject.cant_delete_text2", eObj.toString())); } break; case DBEditObject.DELETING: case DBEditObject.DROPPING: // already to be deleted return null; } try { retVal = eObj.remove(); } catch (Throwable ex) { Ganymede.logError(ex); rollback(ckp_label); return Ganymede.createErrorDialog(this.getGSession(), ts.l("deleteDBObject.error"), ts.l("deleteDBObject.error_text", eObj.toString(), ex.getMessage())); } // the remove logic can entirely bypass our normal finalize logic if (!ReturnVal.didSucceed(retVal)) { if (retVal.getCallback() == null) { // oops, irredeemable failure. rollback. rollback(ckp_label); return retVal; } else { // the remove() logic is presenting a wizard // to the user.. turn the client over to // the wizard return retVal; } } else { // ok, go ahead and finalize.. the finalizeRemove method will // handle doing a rollback or popCheckpoint if necessary // it is essential that we do this call, or else we might // leave namespace handles referencing this object retVal2 = eObj.finalizeRemove(true, ckp_label); return retVal2; } } /** * <p>Inactivate an object in the database</p> * * <p>This method method can only be called in the context of an open * transaction. Because the object must be checked out (which is the only * way to obtain a {@link arlut.csd.ganymede.server.DBEditObject DBEditObject}), * no other locking is required. This method * will take an object out of the {@link arlut.csd.ganymede.server.DBStore DBStore} * and proceed to do whatever is * necessary to cause that object to be 'inactivated'.</p> * * <p>Note that this method does not check to see whether permission * has been obtained to inactivate the object.. that's done in * {@link arlut.csd.ganymede.server.GanymedeSession#inactivate_db_object(arlut.csd.ganymede.common.Invid) * GanymedeSession.inactivate_db_object()}.</p> * * @param eObj An object checked out in the current transaction to be inactivated * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public synchronized ReturnVal inactivateDBObject(DBEditObject eObj) { ReturnVal retVal; String ckp_label; /* -- */ if (editSet == null) { throw new RuntimeException(ts.l("global.notransaction", "inactivateDBObject")); } ckp_label = RandomUtils.getSaltedString("inactivate[" + eObj.getLabel() + "]"); switch (eObj.getStatus()) { case DBEditObject.EDITING: case DBEditObject.CREATING: break; default: return Ganymede.createErrorDialog(this.getGSession(), ts.l("inactivateDBObject.error"), ts.l("inactivateDBObject.error_text")); } checkpoint(ckp_label); if (debug) { System.err.println("DBSession.inactivateDBObject(): Calling eObj.inactivate()"); } try { retVal = eObj.inactivate(ckp_label); } catch (Throwable ex) { Ganymede.logError(ex); // oops, irredeemable failure. rollback. eObj.finalizeInactivate(false, ckp_label); return Ganymede.createErrorDialog(this.getGSession(), ts.l("inactivateDBObject.error"), ts.l("inactivateDBObject.error_text2", eObj.toString(), ex.getMessage())); } if (debug) { System.err.println("DBSession.inactivateDBObject(): Got back from eObj.inactivate()"); } if (!ReturnVal.didSucceed(retVal)) { if (retVal.getCallback() == null) { // oops, irredeemable failure. rollback. System.err.println("DBSession.inactivateDBObject(): object refused inactivation, rolling back"); eObj.finalizeInactivate(false, ckp_label); } // otherwise, we've got a wizard that the client will deal with. } else { // immediate success! eObj.finalizeInactivate(true, ckp_label); } return retVal; } /** * <p>Reactivates an object in the database.</p> * * <p>This method method can only be called in the context of an open * transaction. Because the object must be checked out (which is the only * way to obtain a {@link arlut.csd.ganymede.server.DBEditObject DBEditObject}), * no other locking is required. This method * will take an object out of the {@link arlut.csd.ganymede.server.DBStore DBStore} * and proceed to do whatever is * necessary to cause that object to be 'inactivated'.</p> * * <p>Note that this method does not specifically check to see whether permission * has been obtained to reactivate the object.. that's done in * {@link arlut.csd.ganymede.server.GanymedeSession#reactivate_db_object(arlut.csd.ganymede.common.Invid) * GanymedeSession.reactivate_db_object()}.</p> * * @param eObj An object checked out in the current transaction to be reactivated * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public synchronized ReturnVal reactivateDBObject(DBEditObject eObj) { ReturnVal retVal; String ckp_label; /* -- */ if (editSet == null) { throw new RuntimeException(ts.l("global.notransaction", "reactivateDBObject")); } ckp_label = RandomUtils.getSaltedString("reactivate[" + eObj.getLabel() + "]"); switch (eObj.getStatus()) { case DBEditObject.DELETING: case DBEditObject.DROPPING: return Ganymede.createErrorDialog(this.getGSession(), ts.l("reactivateDBObject.error"), ts.l("reactivateDBObject.error_text")); } if (!eObj.isInactivated()) { return Ganymede.createErrorDialog(this.getGSession(), ts.l("reactivateDBObject.error"), ts.l("reactivateDBObject.error_text2")); } checkpoint(ckp_label); System.err.println(ts.l("reactivateDBObject.debug1")); try { retVal = eObj.reactivate(ckp_label); } catch (Throwable ex) { Ganymede.logError(ex); // oops, irredeemable failure. rollback. rollback(ckp_label); return Ganymede.createErrorDialog(this.getGSession(), ts.l("reactivateDBObject.error"), ts.l("reactivateDBObject.error_text3", eObj.toString(), ex.getMessage())); } System.err.println(ts.l("reactivateDBObject.debug2")); if (!ReturnVal.didSucceed(retVal)) { if (retVal.getCallback() == null) { // oops, irredeemable failure. rollback. System.err.println(ts.l("reactivateDBObject.debug3")); rollback(ckp_label); } } else { // immediate success! eObj.finalizeReactivate(true, ckp_label); } return retVal; } /** * <p>This method, when called on an embedded DBObject, recurses up the embedding * hierarchy to find the top-level embedding object. If the embedded object is * an editable object in the process of being deleted (its status is set to 'DELETING'), * the returned top-level embedding object will be the original, pre-edited version, * so that the original label might be retrieved.</p> */ DBObject getContainingObj(DBObject object) { DBObject localObj = object; while (localObj != null && localObj.isEmbedded()) { if (!localObj.isDefined(SchemaConstants.ContainerField) && (localObj instanceof DBEditObject) && ((DBEditObject) localObj).getStatus() == ObjectStatus.DELETING) { localObj = ((DBEditObject) localObj).getOriginal(); } Invid inv = (Invid) localObj.getFieldValueLocal(SchemaConstants.ContainerField); if (inv == null) { // "getContainingObj() couldn''t find owner of embedded object {0}" throw new IntegrityConstraintException(ts.l("getContainingObj.integrity", object.getLabel())); } localObj = viewDBObject(inv); } return localObj; } /** * <p>This is a method to allow code in the server to quickly and * safely get a full list of objects in an object base.</p> * * <p>This is only a server-side method. getObjects() does * not do anything to check access permissions.</p> * * <p>It is the responsiblity of the code that gets a List * back from this method not to modify the List returned * in any way, as it may be shared by other threads.</p> * * <p>Any objects returned by getObjects() will reflect the * state of that object in this session's transaction, if a * transaction is open.</p> * * @return a List of DBObject references. */ public synchronized List<DBObject> getTransactionalObjects(short baseid) { DBObjectBase base; /* -- */ base = Ganymede.db.getObjectBase(baseid); if (base == null) { try { throw new RuntimeException(ts.l("getTransactionalObjects.no_base", Integer.valueOf(baseid))); } catch (RuntimeException ex) { Ganymede.debug(Ganymede.stackTrace(ex)); return null; } } if (!isTransactionOpen()) { // return a snapshot reference to the base's iteration set return base.getIterationSet(); } else { List<DBObject> iterationSet; Map<Invid, DBEditObject> objects; // grab a snapshot reference to the vector of objects // checked into the database iterationSet = base.getIterationSet(); // grab a snapshot copy of the objects checked out in this transaction objects = editSet.getObjectHashClone(); // and generate our list List<DBObject> results = new ArrayList<DBObject>(iterationSet.size()); for (DBObject obj: iterationSet) { if (objects.containsKey(obj.getInvid())) { results.add(objects.get(obj.getInvid())); } else { results.add(obj); } } // drop our reference to the iterationSet iterationSet = null; // we've recorded any objects that are in the database.. now // look to see if there are any objects that are newly created // in our transaction's object list and add them as well. for (DBEditObject eObj: objects.values()) { if ((eObj.getStatus() == ObjectStatus.CREATING) && (eObj.getTypeID()==baseid)) { results.add(eObj); } } return results; } } /** * <p>Convenience pass-through method</p> * * <p>This method may block if another thread has already checkpointed * this transaction. Checkpoints are intended to be of definite extent, * as the interleaving of checkpoints by multiple threads would lead * to trouble.</p> * * @see arlut.csd.ganymede.server.DBEditSet#checkpoint(java.lang.String) */ public final void checkpoint(String name) { editSet.checkpoint(name); // *synchronized* } /** * <p>Convenience pass-through method</p> * * @see arlut.csd.ganymede.server.DBEditSet#popCheckpoint(java.lang.String) */ public final boolean popCheckpoint(String name) { DBCheckPoint point = null; /* -- */ point = editSet.popCheckpoint(name); // *synchronized* return (point != null); } /** * <p>Convenience pass-through method</p> * * @see arlut.csd.ganymede.server.DBEditSet#rollback(java.lang.String) */ public final boolean rollback(String name) { return editSet.rollback(name); // *synchronized* } /** * <p>Returns true if the session's lock is currently locked, false * otherwise.</p> */ public boolean isLocked(DBLock lockParam) { return lockManager.isLocked(lockParam); } /** * <p>Establishes a read lock for the {@link arlut.csd.ganymede.server.DBObjectBase DBObjectBase}s * in bases.</p> * * <p>The thread calling this method will block until the read lock * can be established. If any of the {@link * arlut.csd.ganymede.server.DBObjectBase DBObjectBases} in the * bases List have transactions currently committing, the * establishment of the read lock will be suspended until all such * transactions are committed.</p> * * <p>All viewDBObject calls done within the context of an open read lock * will be transaction consistent. Other sessions may pull objects out for * editing during the course of the session's read lock, but no visible changes * will be made to those ObjectBases until the read lock is released.</p> */ public DBReadLock openReadLock(List<DBObjectBase> bases) throws InterruptedException { return lockManager.openReadLock(bases); } /** * <p>openReadLock establishes a read lock for the entire * {@link arlut.csd.ganymede.server.DBStore DBStore}.</p> * * <p>The thread calling this method will block until the read lock * can be established. If transactions on the database are * currently committing, the establishment of the read lock will be suspended * until all such transactions are committed.</p> * * <p>All viewDBObject calls done within the context of an open read lock * will be transaction consistent. Other sessions may pull objects out for * editing during the course of the session's read lock, but no visible changes * will be made to those ObjectBases until the read lock is released.</p> */ public DBReadLock openReadLock() throws InterruptedException { return lockManager.openReadLock(); } /** * <p>Establishes a write lock for the {@link * arlut.csd.ganymede.server.DBObjectBase DBObjectBase}s in * bases.</p> * * <p>The thread calling this method will block until the write lock * can be established. If this DBSession already possesses a write * lock, read lock, or dump lock, the openWriteLock() call will fail * with an InterruptedException.</p> * * <p>If one or more different DBSessions (besides this) have locks * in place that would block acquisition of the write lock, this * method will block until the lock can be acquired.</p> */ public DBWriteLock openWriteLock(List<DBObjectBase> bases) throws InterruptedException { return lockManager.openWriteLock(bases); } /** * <p>This method establishes a dump lock on all object bases in this Ganymede * server.</p> */ public DBDumpLock openDumpLock() throws InterruptedException { return lockManager.openDumpLock(); } /** * <p>releaseLock releases a particular lock held by this session. * This method will not force a lock being held by another thread to * drop out of its establish method.. it is intended to be called by * the same thread that established the lock.</p> */ public void releaseLock(DBLock lock) { lockManager.releaseLock(lock); } /** * Releases all DBLocks held by this DBSessions. */ private void releaseAllLocks() { lockManager.releaseAllLocks(); } /** * <p>openTransaction establishes a transaction context for this * session. When this method returns, the session can call * editDBObject() and createDBObject() to obtain {@link * arlut.csd.ganymede.server.DBEditObject DBEditObject}s. Methods * can then be called on the DBEditObjects to make changes to the * database. These changes are actually performed when and if * commitTransaction() is called.</p> * * @param describe An optional string containing a comment to be * stored in the modification history for objects modified by this * transaction. * * @see arlut.csd.ganymede.server.DBEditObject */ public void openTransaction(String describe) { this.openTransaction(describe, true); } /** * <p>openTransaction establishes a transaction context for this session. * When this method returns, the session can call editDBObject() and * createDBObject() to obtain {@link arlut.csd.ganymede.server.DBEditObject DBEditObject}s. * Methods can then be called * on the DBEditObjects to make changes to the database. These changes * are actually performed when and if commitTransaction() is called.</p> * * @param describe An optional string containing a comment to be * stored in the modification history for objects modified by this * transaction. * * @param interactive If false, this transaction will operate in * non-interactive mode. Certain Invid operations will be optimized * to avoid doing choice list queries and bind checkpoint * operations. When a transaction is operating in non-interactive mode, * any failure that cannot be handled cleanly due to the optimizations will * result in the transaction refusing to commit when commitTransaction() * is attempted. This mode is intended for batch operations. * * @see arlut.csd.ganymede.server.DBEditObject */ public synchronized void openTransaction(String describe, boolean interactive) { if (editSet != null) { throw new IllegalArgumentException(ts.l("openTransaction.transaction")); } editSet = new DBEditSet(store, this, describe, interactive); } /** * <p>commitTransaction causes any changes made during the context of * a current transaction to be performed. Appropriate portions of the * database are locked, changes are made to the in-memory image of * the database, and a description of the changes is placed in the * {@link arlut.csd.ganymede.server.DBStore DBStore} journal file. Event * logging / mail notification may take place.</p> * * <p>The session must not hold any locks when commitTransaction is * called. The symmetrical invid references between related objects * and the atomic namespace management code should guarantee that no * incompatible change is made with respect to any checked out objects * while the Bases are unlocked.</p> * * @return null if the transaction was committed successfully, * a non-null ReturnVal if there was a commit failure. * * @see arlut.csd.ganymede.server.DBEditObject */ public ReturnVal commitTransaction() { return commitTransaction(null); } /** * <p>commitTransaction causes any changes made during the context of * a current transaction to be performed. Appropriate portions of the * database are locked, changes are made to the in-memory image of * the database, and a description of the changes is placed in the * {@link arlut.csd.ganymede.server.DBStore DBStore} journal file. Event * logging / mail notification may take place.</p> * * <p>The session must not hold any locks when commitTransaction is * called. The symmetrical invid references between related objects * and the atomic namespace management code should guarantee that no * incompatible change is made with respect to any checked out objects * while the Bases are unlocked.</p> * * @param comment If not null, a comment to attach to logging and * email generated in response to this transaction. * * @return null if the transaction was committed successfully, * a non-null ReturnVal if there was a commit failure. * * @see arlut.csd.ganymede.server.DBEditObject */ public synchronized ReturnVal commitTransaction(String comment) { ReturnVal retVal = null; /* -- */ // we need to release our readlock, if we have one, // so that the commit can establish a writelock.. // should i make it so that a writelock can be established // if the possessor of a readlock doesn't give it up? if (debug) { // "{0}: entering commitTransaction" System.err.println(ts.l("commitTransaction.debug1", String.valueOf(key))); } if (editSet == null) { // "{0}:commitTransaction called outside of a transaction" throw new RuntimeException(ts.l("commitTransaction.notransaction", String.valueOf(key))); } // we can't commit a transaction with locks held, because that // might lead to deadlock. we release all locks now, then when we // call editSet.commit(), that will attempt to establish whatever // write locks we need, for the duration of the commit() call. releaseAllLocks(); if (debug) { // "{0}: committing editset" System.err.println(ts.l("commitTransaction.debug2", String.valueOf(key))); } String description = editSet.description; // get before commit() clears it retVal = editSet.commit(comment); // *synchronized* if (ReturnVal.didSucceed(retVal)) { if (description != null) { // "{0}: committed transaction {1}" Ganymede.debug(ts.l("commitTransaction.debug3", String.valueOf(key), description)); } else { // "{0}: committed transaction" Ganymede.debug(ts.l("commitTransaction.debug4", String.valueOf(key))); } editSet = null; } else { // The DBEditSet.commit() method will set retVal.doNormalProcessing true // if the problem that prevented commit was transient.. i.e., missing // fields, lock not available, etc. // If we had an IO error or some unexpected exception or the // like, doNormalProcessing will be false, and the transaction // will have been wiped out by the commit logic. In this case, // there's nothing that can be done, the transaction is dead // and gone. if (!retVal.doNormalProcessing) { editSet = null; } } return retVal; // later on we'll figure out how to do this right } /** * <p>abortTransaction causes all {@link arlut.csd.ganymede.server.DBEditObject DBEditObject}s * that were pulled during * the course of the session's transaction to be released without affecting * the state of the database. Any changes made to * {@link arlut.csd.ganymede.server.DBObject DBObject}s pulled for editing * by this session during this transaction are abandoned. Any objects created * or destroyed by this session during this transaction are abandoned / unaffected * by the actions during the transaction.</p> * * <p>Calling abortTransaction() has no affect on any locks held by * this session, but generally no locks should be held here. * abortTransaction() will attempt to abort a write lock being * established by a commitTransaction() call on another thread.</p> * * @return null if the transaction was committed successfully, * a non-null ReturnVal if there was a commit failure. * * @see arlut.csd.ganymede.server.DBEditObject */ public synchronized ReturnVal abortTransaction() { if (editSet == null) { throw new RuntimeException(ts.l("global.notransaction", "abortTransaction")); } if (!editSet.abort()) { Ganymede.debug(ts.l("abortTransaction.cant_abort", String.valueOf(key))); return Ganymede.createErrorDialog(this.getGSession(), ts.l("abortTransaction.error"), ts.l("abortTransaction.error_text")); } else { editSet = null; // for gc } return null; } /** * <p>Returns true if this session has an transaction open</p> */ public boolean isTransactionOpen() { return (editSet != null); } /** * <p>This method returns true if this session is carrying out a * transaction on behalf of an interactive client.</p> */ public boolean isInteractive() { return editSet.isInteractive(); } /** * <p>Simple accessor to allow us to trap derefence later if we want.</p> */ public DBEditSet getEditSet() { return editSet; } /** * <p>This method returns a handle to the objectHook for * a particular Invid.</p> */ public DBEditObject getObjectHook(Invid invid) { DBObjectBase base; base = Ganymede.db.getObjectBase(invid.getType()); return base.getObjectHook(); } /** * <p>Gets our lock key</p> */ public Object getKey() { return key; } /** * <p>This method is responsible for providing an identifier string * for the user who this session belongs to, and is used for * logging and what-not.</p> */ public String getID() { return GSession.getPermManager().getIdentity(); } /** * <p>This method returns a handle to the Ganymede Session * that owns this DBSession.</p> */ public GanymedeSession getGSession() { return GSession; } /** * Returns the label of a given Invid in this session. * * @return null if the Invid is not found in this session */ public String getObjectLabel(Invid invid) { try { return viewDBObject(invid).getLabel(); } catch (NullPointerException ex) { return null; } } /** * Returns the label of a given Invid committed into the database. * * @throws NullPointerException if invid could not be found. */ public String getCommittedObjectLabel(Invid invid) { return store.getObject(invid).getLabel(); } /** * This method is intended as a lightweight way of returning a * handy description of the type and label of the specified invid. * No locking is done, and the label returned will be viewed through * the context of the current transaction, if any. */ public String describe(Invid invid) { try { DBObject obj = viewDBObject(invid); if (obj != null) { return obj.getTypeName() + " " + obj.getLabel(); } else { DBObjectBase base = Ganymede.db.getObjectBase(invid.getType()); return base.getName() + " " + invid.toString() + " (non-existing)"; } } catch (NullPointerException ex) { return null; } } public String toString() { if (editSet != null) { return "DBSession[" + editSet.description + "]"; } else { return super.toString(); } } //****************************************************************** // // To satisfy the arlut.csd.ganymede.common.QueryDescriber interface // //****************************************************************** /** * This method is intended as a lightweight way of returning a handy * description of the specified type. */ public String describeType(short type) { try { DBObjectBase base = Ganymede.db.getObjectBase(type); return base.getName(); } catch (Exception ex) { return String.valueOf(type); } } public String describeField(short objType, short fieldType) { try { DBObjectBase base = Ganymede.db.getObjectBase(objType); DBObjectBaseField field = base.getField(fieldType); return field.getName(); } catch (Exception ex) { return String.valueOf(fieldType); } } public String describeField(String objTypeName, short fieldType) { try { DBObjectBase base = Ganymede.db.getObjectBase(objTypeName); DBObjectBaseField field = base.getField(fieldType); return field.getName(); } catch (Exception ex) { return String.valueOf(fieldType); } } }