/* GASH 2 DBEditObject.java The GANYMEDE object storage system. Created: 2 July 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.io.IOException; import java.rmi.RemoteException; import java.util.ArrayList; import java.util.Date; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Vector; import arlut.csd.JDialog.JDialogBuff; import arlut.csd.Util.booleanSemaphore; import arlut.csd.Util.XMLUtils; import arlut.csd.ganymede.common.GanyPermissionsException; import arlut.csd.ganymede.common.Invid; import arlut.csd.ganymede.common.NotLoggedInException; import arlut.csd.ganymede.common.ObjectStatus; import arlut.csd.ganymede.common.PermEntry; import arlut.csd.ganymede.common.Query; import arlut.csd.ganymede.common.QueryDataNode; import arlut.csd.ganymede.common.QueryNode; import arlut.csd.ganymede.common.QueryNotNode; import arlut.csd.ganymede.common.QueryResult; import arlut.csd.ganymede.common.ReturnVal; import arlut.csd.ganymede.common.SchemaConstants; import arlut.csd.ganymede.rmi.Session; import arlut.csd.ganymede.rmi.db_field; import arlut.csd.Util.TranslationService; /*------------------------------------------------------------------------------ class DBEditOBject ------------------------------------------------------------------------------*/ /** * <p>DBEditObject is the main base class that is subclassed by individual * application object types to provide editing and management intelligence. * Both static and instance methods are defined in DBEditObject which can * be subclassed to provide object management intelligence.</p> * * <p>A instance of DBEditObject is a copy of a DBObject that has been * exclusively checked out from the main database so that a * {@link arlut.csd.ganymede.server.DBSession DBSession} * can edit the fields of the object. The DBEditObject class keeps * track of the changes made to fields, keeping things properly * synchronized with unique field name spaces.</p> * * <p>Generally, DBEditObjects are obtained in the context of a * {@link arlut.csd.ganymede.server.DBEditSet DBEditSet} transaction object. When * the DBEditSet is committed, a new {@link arlut.csd.ganymede.server.DBObject DBObject} * is created from the contents of the DBEditObject and is made to replace the * original object in the DBStore. If the EditSet is aborted, the * DBEditObject is dropped.</p> * * <p>There is one case, however, in which a DBEditObject will be * present in the server outside of a DBEditSet context, and that is * the DBEditObject instance used for the {@link * arlut.csd.ganymede.server.DBObjectBase DBObjectBase}'s {@link * arlut.csd.ganymede.server.DBObjectBase#objectHook objectHook} * customization object. In this case, a DBEditObject of the * appropriate subclass is created using the {@link * arlut.csd.ganymede.server.DBEditObject#DBEditObject(arlut.csd.ganymede.server.DBObjectBase) * first constructor variant}. A wide variety of methods in the * server will make method calls on the DBObjectBase objectHook to * allow a custom DBEditObject subclass to customize the server's * behavior. Such methods are labeled <b>*PSEUDOSTATIC*</b>, which * means that those methods are designed not to examine or report on * the internal state of the objectHook, but rather are meant to * operate based only on parameters passed into the method. These * methods are PSEUDOSTATIC rather than static because if they were * true static methods, every place in the server where such methods * are called would have to use the relatively cumbersome Java * Reflection API rather than being able to call methods on a * instance of the appropriate DBEditObject subclass.</p> * * <p>See the <a href="../../../../../customization/index.html" * target="_top">DBEditObject subclassing guide</a> for more * information generally on DBEditObject customization.</p> * * <p><b>IMPORTANT PROGRAMMING NOTE!</b>: It is critical that * synchronized methods in DBEditObject and in subclasses thereof do not * call synchronized methods in DBSession, as there is a strong possibility * of nested monitor deadlocking.</p> * * @author Jonathan Abbey, jonabbey@arlut.utexas.edu, ARL:UT */ public class DBEditObject extends DBObject implements ObjectStatus { static boolean debug = false; /** * TranslationService object for handling string localization in * the Ganymede server. */ static final TranslationService ts = TranslationService.getTranslationService("arlut.csd.ganymede.server.DBEditObject"); public final static int FIRSTOP = 0; public final static int SETVAL = 1; public final static int SETELEMENT = 2; public final static int ADDELEMENT = 3; public final static int DELELEMENT = 4; public final static int ADDELEMENTS = 5; public final static int DELELEMENTS = 6; public final static int SETPASSPLAIN = 7; public final static int SETPASSCRYPT = 8; public final static int SETPASSMD5 = 9; public final static int SETPASSWINHASHES = 10; public final static int SETPASSAPACHEMD5 = 11; public final static int SETPASSSSHA = 12; public final static int SETPASS_SHAUNIXCRYPT = 13; public final static int SETPASS_BCRYPT = 14; public final static int LASTOP = 14; public final static Date minDate = new Date(Long.MIN_VALUE); public final static Date maxDate = new Date(Long.MAX_VALUE); public final static void setDebug(boolean val) { debug = val; } /* --------------------- Instance fields and methods --------------------- */ /** * Unless this DBEditObject represents a newly created Ganymede * object, we'll have a reference to the original DBObject which is * currently registered in the DBStore. Only one DBEditObject can * be connected to a DBObject at a time, giving us object-level * locking. */ protected DBObject original; /** * true if this object has had its commitPhase1() method called, but * has not yet had its commitPhase2() or release() methods called. * If commitSemaphore is set to true, the DBField.isEditable() * method will always return false for fields in this object, and no * editing will be allowed on this object. */ private booleanSemaphore commitSemaphore = new booleanSemaphore(false); /** * true if the object is in the middle of carrying out deletion * logic.. consulted by subclasses by way of the {@link * arlut.csd.ganymede.server.DBEditObject#isDeleting()} method to * determine whether they should object to fields being set to null */ private boolean deleting = false; /** * tracks this object's editing status. See * {@link arlut.csd.ganymede.common.ObjectStatus ObjectStatus}. */ private byte status; /** * true if the object has a version currently * stored in the DBStore */ final boolean stored; /** * transaction that this object has been checked out in * care of. */ public DBEditSet editset; /* -- */ /** * <p>Dummy constructor, is responsible for creating a DBEditObject * strictly for the purpose of having a handle to call our * pseudostatic customization methods on.</p> * * <p>This is the version of the constructor that the {@link * arlut.csd.ganymede.server.DBObjectBase DBObjectBase}'s {@link * arlut.csd.ganymede.server.DBObjectBase#createHook() createHook()} * method uses to create the {@link * arlut.csd.ganymede.server.DBObjectBase#objectHook objectHook} * object.</p> */ public DBEditObject(DBObjectBase base) { super(base); this.stored = false; this.editset = null; // this will be our cue to our static handle status for our methods } /** * <p>Creation constructor, is responsible for creating a new * editable object with all fields listed in the {@link * arlut.csd.ganymede.server.DBObjectBaseField DBObjectBaseField} * instantiated but undefined.</p> * * <p>This constructor is not really intended to be called via * super() from subclass constructors. Creation time field value * initialization is to be handled by initializeNewObject().</p> * * @see arlut.csd.ganymede.server.DBField */ public DBEditObject(DBObjectBase objectBase, Invid invid, DBEditSet editset) { super(objectBase, invid, editset); this.original = null; this.editset = editset; commitSemaphore.set(false); this.stored = false; this.status = CREATING; } /** * <p>Check-out constructor, used by {@link * arlut.csd.ganymede.server.DBObject#createShadow(arlut.csd.ganymede.server.DBEditSet) * DBObject.createShadow()} to pull out an object for editing.</p> */ public DBEditObject(DBObject original, DBEditSet editset) { super(original.objectBase, original.getID(), editset.getDBSession().getGSession()); /* -- */ this.editset = editset; this.commitSemaphore.set(false); this.stored = true; this.status = EDITING; this.original = original; int i = 0; DBField newFields[] = new DBField[objectBase.getFieldCount()]; for (DBObjectBaseField fieldDef: objectBase.getFieldsInFieldOrder()) { short id = fieldDef.getID(); DBField originalField = original.getField(id); DBField copyField; if (originalField != null) { copyField = DBField.copyField(this, originalField); } else { copyField = DBField.createTypedField(this, fieldDef); } newFields[i++] = copyField; } this.setAllFields(newFields); } /** * <p>Returns the transaction object owning this object, or * null if an unowned data object.</p> * * <p>Note that this is public, but not made available * to the client via a remote interface.</p> */ public final DBEditSet getEditSet() { return editset; } /** * Returns the original version of the object that we were created * to edit. If we are a newly created object, this method will * return null. */ @Override public final DBObject getOriginal() { return original; } /** * Returns a code indicating whether this object * is being created, edited, or deleted. * * @see arlut.csd.ganymede.common.ObjectStatus#CREATING * @see arlut.csd.ganymede.common.ObjectStatus#EDITING * @see arlut.csd.ganymede.common.ObjectStatus#DELETING * @see arlut.csd.ganymede.common.ObjectStatus#DROPPING */ public final byte getStatus() { return status; } /** * This method returns true if this DBEditObject is in the middle * of handling clean up during object deletion. */ public final boolean isDeleting() { return deleting; } /** * We'll use DBObject's identity-based equals */ @Override public boolean equals(Object param) { return super.equals(param); } /** * Returns the primary label of this object. * * @see arlut.csd.ganymede.rmi.db_object */ @Override public final String getLabel() { if (getStatus() == DELETING) { return getOriginal().getLabel(); } return super.getLabel(); } /** * <p>Returns the primary label of this remote object.</p> * * <p>For convenience in custom DBEditObject subclasses.</p> */ public final String getLabel(Invid invid) { return getDBSession().getObjectLabel(invid); } /** * <p>If this object type is embedded, this method will return the * desired display label for the embedded object.</p> * * <p>This label may not be the same as returned by getLabel(), which * is guaranteed to be derived from a namespace constrained label * field, suitable for use in the XML context.</p> * * @see arlut.csd.ganymede.rmi.db_object */ @Override public final String getEmbeddedObjectDisplayLabel() { if (getStatus() == DELETING) { return getOriginal().getEmbeddedObjectDisplayLabel(); } return super.getEmbeddedObjectDisplayLabel(); } /** * <p>If this DBEditObject is managing an embedded object, the * getEmbeddedObjectLabel() can be overridden to display a synthetic * label in the context of viewing or editing the containing object, * and when doing queries on the containing type.</p> * * <p>The getLabel() method will not consult this hook, however, and * embedded objects will be represented with their unique label * field when processed in an XML context.</p> * * <b>*PSEUDOSTATIC*</b> */ public String getEmbeddedObjectDisplayLabelHook(DBObject object) { return object.getLabel(); } /** * <p>This method is used to make sure that the built-in fields that * the server assumes will always be present in any editable object * will be in place.</p> * * <p>This method checks with instantiateNewField() if the field id is * not one of those that is needfull. If instantiateNewField() approves * the creation of a new field, checkNewField() will check to see if * the {@link arlut.csd.ganymede.server.GanymedeSession GanymedeSession}'s * permissions permit the field creation.</p> */ public final boolean checkNewField(short fieldID) { if (fieldID <= SchemaConstants.FinalSystemField) { return true; // we always allow the built in fields } return instantiateNewField(fieldID); } /** * Sets this object's status code * * @see arlut.csd.ganymede.common.ObjectStatus#CREATING * @see arlut.csd.ganymede.common.ObjectStatus#EDITING * @see arlut.csd.ganymede.common.ObjectStatus#DELETING * @see arlut.csd.ganymede.common.ObjectStatus#DROPPING */ final void setStatus(byte new_status) { switch (new_status) { case CREATING: case EDITING: case DELETING: case DROPPING: status = new_status; break; default: throw new RuntimeException("unrecognized status code"); } } /** * <p>Shortcut method to set a field's value. Using this * method can save the client a roundtrip to the server.</p> * * <p>This method cannot be used on permission fields or password * fields.</p> * * @see arlut.csd.ganymede.rmi.db_object */ @Override public final ReturnVal setFieldValue(short fieldID, Object value) throws GanyPermissionsException { DBField field = getField(fieldID); /* -- */ if (field != null) { return field.setValue(value); } // "DBEditObject.setFieldValue() error" // "DBEditObject.setFieldValue() couldn''t find field {0} in object {1}" return Ganymede.createErrorDialog(this.getGSession(), ts.l("setFieldValue.error"), ts.l("setFieldValue.errorTxt", Short.valueOf(fieldID), getLabel())); } /** * <p>Shortcut method to set a field's value. This version bypasses * permission checking and is only intended for server-side * use.</p> * * <p>This method cannot be used on permission fields or password * fields.</p> */ public final ReturnVal setFieldValueLocal(short fieldID, Object value) { DBField field = getField(fieldID); /* -- */ if (field != null) { return field.setValueLocal(value); } // "DBEditObject.setFieldValueLocal() error" // "DBEditObject.setFieldValueLocal() couldn''t find field {0} in object {1}" return Ganymede.createErrorDialog(this.getGSession(), ts.l("setFieldValueLocal.error"), ts.l("setFieldValueLocal.errorTxt", Short.valueOf(fieldID), getLabel())); } /** * Returns true if the object has ever been stored in the * {@link arlut.csd.ganymede.server.DBStore DBStore} under the * current invid. */ public final boolean isStored() { return stored; } /* -------------------- pseudo-static Customization hooks -------------------- The following block of methods are intended to be used in static fashion.. that is, a DBObjectBase can load in a class that extends DBEditObjectBase and hold an instance of such as DBObjectBase.objectHook. The following methods are used in a static fashion, that is they are intended to perform actions on designated external DBObjects rather than on the 'this' per-DBObjectBase objectHook instance, which may not be fully defined. */ /** * <p>This method is used to control whether or not it is acceptable to * make a link to the given field in this * {@link arlut.csd.ganymede.server.DBObject DBObject} type when the * user only has editing access for the source * {@link arlut.csd.ganymede.server.InvidDBField InvidDBField} and not * the target.</p> * * <p>This version of anonymousLinkOK takes additional parameters * to allow an object type to decide that it does or does not want * to allow a link based on what field of what object wants to link * to it.</p> * * <p>By default, the 3 variants of the DBEditObject * anonymousLinkOK() method are chained together, so that the * customizer can choose which level of detail he is interested in. * {@link arlut.csd.ganymede.server.InvidDBField InvidDBField}'s * {@link * arlut.csd.ganymede.server.InvidDBField#bind(arlut.csd.ganymede.common.Invid,arlut.csd.ganymede.common.Invid,boolean) * bind()} method calls this version. This version calls the three * parameter version, which calls the two parameter version, which * returns false by default. Customizers can implement any of the * three versions, but unless you maintain the version chaining * yourself, there's no point to implementing more than one of * them.</p> * * <p>Note that the {@link * arlut.csd.ganymede.server.DBEditObject#choiceListHasExceptions(arlut.csd.ganymede.server.DBField) * choiceListHasExceptions()} method will call this version of * anonymousLinkOK() with a null targetObject, to determine that the * client should not use its cache for an InvidDBField's choices. * Any overriding done of this method must be able to handle a null * targetObject, or else an exception will be thrown * inappropriately.</p> * * <p>The only reason to consult targetObject in any case is to * allow or disallow anonymous object linking to a field based on * the current state of the target object. If you are just writing * generic anonymous linking rules for a field in this object type, * targetObject won't concern you anyway. If you do care about the * targetObject's state, though, you have to be prepared to handle * a null valued targetObject.</p> * * <p><b>*PSEUDOSTATIC*</b></p> * * @param targetObject The object that the link is to be created in (may be null) * @param targetFieldID The field that the link is to be created in * @param sourceObject The object on the other side of the proposed link * @param sourceFieldID The field on the other side of the proposed link * @param gsession Who is trying to do this linking? */ public boolean anonymousLinkOK(DBObject targetObject, short targetFieldID, DBObject sourceObject, short sourceFieldID, GanymedeSession gsession) { // by default, dispatch to the more generic approval method return anonymousLinkOK(targetObject, targetFieldID, gsession); } /** * <p>This method is used to control whether or not it is acceptable to * rescind a link to the given field in this * {@link arlut.csd.ganymede.server.DBObject DBObject} type when the * user only has editing access for the source * {@link arlut.csd.ganymede.server.InvidDBField InvidDBField} and not * the target.</p> * * <p>This version of anonymousUnlinkOK takes additional parameters * to allow an object type to decide that it does or does not want * to allow a link to be rescinded based on what field of what * object wants to unlink from it.</p> * * <p>By default, the 3 variants of the DBEditObject anonymousUnlinkOK() * method are chained together, so that the customizer can choose * which level of detail he is interested in. * {@link arlut.csd.ganymede.server.InvidDBField InvidDBField}'s * {@link arlut.csd.ganymede.server.InvidDBField#unbind(arlut.csd.ganymede.common.Invid,boolean) unbind()} * method calls this version. This version calls the three parameter * version, which calls the two parameter version, which returns * true by default. Customizers can implement any of the three * versions, but unless you maintain the version chaining yourself, * there's no point to implementing more than one of them.</p> * * <p><b>*PSEUDOSTATIC*</b></p> * * @param targetObject The object that the link is removed from * @param targetFieldID The field that the link is removed from * @param sourceObject The object on the other side of the existing link * @param sourceFieldID The object on the other side of the existing link * @param gsession Who is trying to do this unlinking? */ public boolean anonymousUnlinkOK(DBObject targetObject, short targetFieldID, DBObject sourceObject, short sourceFieldID, GanymedeSession gsession) { // by default, dispatch to the more generic approval method return anonymousUnlinkOK(targetObject, targetFieldID, gsession); } /** * <p>This method is used to control whether or not it is acceptable to * make a link to the given field in this * {@link arlut.csd.ganymede.server.DBObject DBObject} type when the * user only has editing access for the source * {@link arlut.csd.ganymede.server.InvidDBField InvidDBField} and not * the target.</p> * * <p>See {@link arlut.csd.ganymede.server.DBEditObject#anonymousLinkOK(arlut.csd.ganymede.server.DBObject,short, * arlut.csd.ganymede.server.DBObject,short,arlut.csd.ganymede.server.GanymedeSession) * anonymousLinkOK(obj,short,obj,short,GanymedeSession)} for details on * anonymousLinkOK() method chaining.</p> * * <p>Note that the {@link arlut.csd.ganymede.server.DBEditObject#choiceListHasExceptions(arlut.csd.ganymede.server.DBField) * choiceListHasExceptions()} method will call this version of anonymousLinkOK() * with a null targetObject, to determine that the client should not * use its cache for an InvidDBField's choices. Any overriding done * of this method must be able to handle a null targetObject, or else * an exception will be thrown inappropriately.</p> * * <p>The only reason to consult targetObject in any case is to * allow or disallow anonymous object linking to a field based on * the current state of the target object. If you are just writing * generic anonymous linking rules for a field in this object type, * targetObject won't concern you anyway. If you do care about the * targetObject's state, though, you have to be prepared to handle * a null valued targetObject.</p> * * <p><b>*PSEUDOSTATIC*</b></p> * * @param targetObject The object that the link is to be created in (may be null) * @param targetFieldID The field that the link is to be created in * @param gsession Who is trying to do this linking? */ public boolean anonymousLinkOK(DBObject targetObject, short targetFieldID, GanymedeSession gsession) { return anonymousLinkOK(targetObject, targetFieldID); } /** * <p>This method is used to control whether or not it is acceptable to * rescind a link to the given field in this * {@link arlut.csd.ganymede.server.DBObject DBObject} type when the * user only has editing access for the source * {@link arlut.csd.ganymede.server.InvidDBField InvidDBField} and not * the target.</p> * * <p>By default, the server always allows anonymous unlinking. * Overriding this method is only required when you want to DISallow * such unlinking.</p> * * <p>See {@link arlut.csd.ganymede.server.DBEditObject#anonymousUnlinkOK(arlut.csd.ganymede.server.DBObject,short, * arlut.csd.ganymede.server.DBObject,short,arlut.csd.ganymede.server.GanymedeSession) * anonymousUnlinkOK(obj,short,obj,short,GanymedeSession)} for details on * anonymousUnlinkOK() method chaining.</p> * * <p><b>*PSEUDOSTATIC*</b></p> * * @param object The object that the link is to be removed from * @param fieldID The field that the linkk is to be removed from * @param gsession Who is trying to do this unlinking? */ public boolean anonymousUnlinkOK(DBObject object, short fieldID, GanymedeSession gsession) { return anonymousUnlinkOK(object, fieldID); } /** * <p>This method is used to control whether or not it is acceptable to * make a link to the given field in this * {@link arlut.csd.ganymede.server.DBObject DBObject} type when the * user only has editing access for the source * {@link arlut.csd.ganymede.server.InvidDBField InvidDBField} and not * the target.</p> * * <p>See {@link arlut.csd.ganymede.server.DBEditObject#anonymousLinkOK(arlut.csd.ganymede.server.DBObject,short, * arlut.csd.ganymede.server.DBObject,short,arlut.csd.ganymede.server.GanymedeSession) * anonymousLinkOK(obj,short,obj,short,GanymedeSession)} for details on * anonymousLinkOK() method chaining.</p> * * <p>Note that the {@link * arlut.csd.ganymede.server.DBEditObject#choiceListHasExceptions(arlut.csd.ganymede.server.DBField) * choiceListHasExceptions()} method will call this version of anonymousLinkOK() * with a null targetObject, to determine that the client should not * use its cache for an InvidDBField's choices. Any overriding done * of this method must be able to handle a null targetObject, or else * an exception will be thrown inappropriately.</p> * * <p>The only reason to consult targetObject in any case is to * allow or disallow anonymous object linking to a field based on * the current state of the target object. If you are just writing * generic anonymous linking rules for a field in this object type, * targetObject won't concern you anyway. If you do care about the * targetObject's state, though, you have to be prepared to handle * a null valued targetObject.</p> * * <p><b>*PSEUDOSTATIC*</b></p> * * @param targetObject The object that the link is to be created in (may be null) * @param targetFieldID The field that the link is to be created in */ public boolean anonymousLinkOK(DBObject targetObject, short targetFieldID) { return false; // by default, permission is denied } /** * <p>This method is used to control whether or not it is acceptable to * rescind a link to the given field in this * {@link arlut.csd.ganymede.server.DBObject DBObject} type when the * user only has editing access for the source * {@link arlut.csd.ganymede.server.InvidDBField InvidDBField} and not * the target.</p> * * <p>By default, the server always allows anonymous unlinking. * Overriding this method is only required when you want to DISallow * such unlinking.</p> * * <p>See {@link * arlut.csd.ganymede.server.DBEditObject#anonymousUnlinkOK(arlut.csd.ganymede.server.DBObject,short, * arlut.csd.ganymede.server.DBObject,short,arlut.csd.ganymede.server.GanymedeSession) * anonymousUnlinkOK(obj,short,obj,short,GanymedeSession)} for details on * anonymousUnlinkOK() method chaining.</p> * * <p><b>*PSEUDOSTATIC*</b></p> * * @param object The object that the link is to be removed from * @param fieldID The field that the linkk is to be removed from */ public boolean anonymousUnlinkOK(DBObject object, short fieldID) { return true; // by default, permission is granted to unlink } /** * <p>Customization method to allow this Ganymede object type to * override the default permissions mechanism for special * purposes.</p> * * <p>If this method returns null, the default permissions mechanism * will be followed. If not, the permissions system will grant * the permissions specified by this method for access to the * given object, and no further elaboration of the permission * will be performed. Note that this override capability does * not apply to operations performed in supergash mode.</p> * * <p>Note as well that this permOverride() has no effect when * creating new objects of this type. Take a look at overriding * {@link arlut.csd.ganymede.server.DBEditObject#canCreate(arlut.csd.ganymede.rmi.Session) canCreate()} * if you need to provide an exception to the normal permissions * system for creating new objects.</p> * * <p>This method should be used very sparingly.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public PermEntry permOverride(GanymedeSession session, DBObject object) { return null; } /** * <p>Customization method to allow this Ganymede object type to grant * permissions above and beyond the default permissions mechanism * for special purposes.</p> * * <p>If this method returns null, the default permissions mechanism * will be followed. If not, the permissions system will grant * the union of the permissions specified by this method for access to the * given object.</p> * * <p>Note as well that this permExpand() has no effect when * creating new objects of this type. Take a look at overriding * {@link arlut.csd.ganymede.server.DBEditObject#canCreate(arlut.csd.ganymede.rmi.Session) canCreate()} * if you need to provide an exception to the normal permissions * system for creating new objects.</p> * * <p>This method is essentially different from permOverride() in that * the permissions system will not just take the result of this * method for an answer, but will grant additional permissions as * appropriate.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public PermEntry permExpand(GanymedeSession session, DBObject object) { return null; } /** * <p>Customization method to allow this Ganymede object type to * override the default permissions mechanism for special * purposes.</p> * * <p>If this method returns null, the default permissions mechanism * will be followed. If not, the permissions system will grant the * permissions specified by this method for access to the given * field, and no further elaboration of the permission will be * performed. If permOverride() returns a non-null value for a * given field, permExpand() will not be consulted for that field. * Just as with permExpand(), this method can never cause greater * permissions to be granted to a field than is available to the * object as a whole, and this override capability does not * apply to operations performed in supergash mode.</p> * * <p>This method should be used very sparingly.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public PermEntry permOverride(GanymedeSession session, DBObject object, short fieldid) { return null; } /** * <p>Customization method to allow this Ganymede object type to grant * permissions above and beyond the default permissions mechanism * for special purposes.</p> * * <p>If this method returns null, the default permissions mechanism * will be followed. If not, the permissions system will grant the * union of the permissions returned by this method and those * determined normally by GanymedeSession's default field * permissions logic. This method can never cause greater * permissions to be granted to a field than is available to the * object as a whole, and the results of permExpand() will have * no effect on operations performed in supergash mode.</p> * * <p>This method is essentially different from permOverride() in that * the permissions system will not just take the result of this * method for an answer, but will grant additional permissions as * appropriate.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public PermEntry permExpand(GanymedeSession session, DBObject object, short fieldid) { return null; } /** * <p>Customization method to verify overall consistency of a * DBObject. This method is intended to be overridden in * DBEditObject subclasses, and will be called by {@link * arlut.csd.ganymede.server.DBEditObject#commitPhase1() * commitPhase1()} to verify the readiness of this object for * commit. The DBObject passed to this method will be a * DBEditObject, complete with that object's GanymedeSession * reference if this method is called during transaction commit, and * that session reference may be used by the verifying code if the * code needs to access the database.</p> * * <p>This method is for custom checks specific to custom * DBEditObject subclasses. Standard checking for missing fields * for which fieldRequired() returns true is done by {@link * arlut.csd.ganymede.server.DBEditSet#commit_checkObjectMissingFields(arlut.csd.ganymede.server.DBEditObject)} * during {@link * arlut.csd.ganymede.server.DBEditSet#commit_handlePhase1()}.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public ReturnVal consistencyCheck(DBObject object) { return null; } /** * <p>Customization method to control whether a specified field * is required to be defined at commit time for a given object.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p>Note that this method will not be called if the controlling * GanymedeSession's enableOversight is turned off, as in * bulk loading.</p> * * <p>Note as well that the designated label field for objects are * always required, whatever this method returns, and that this * requirement holds without regard to the GanymedeSession's * enableOversight value.</p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public boolean fieldRequired(DBObject object, short fieldid) { return false; } /** * <p>Customization method to verify whether the user has permission * to view a given object. The client's DBSession object * will call this per-class method to do an object type- * sensitive check to see if this object feels like being * available for viewing to the client.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public boolean canRead(DBSession session, DBObject object) { return true; } /** * <p>Customization method to verify whether the user has permission * to edit a given object. The client's * {@link arlut.csd.ganymede.server.DBSession DBSession} object * will call this per-class method to do an object type- * sensitive check to see if this object feels like being * available for editing by the client.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public boolean canWrite(DBSession session, DBObject object) { return true; } /** * <p>Customization method to verify whether the user should be able to * see a specific field in a given object. Instances of * {@link arlut.csd.ganymede.server.DBField DBField} will * wind up calling up to here to let us override the normal visibility * process.</p> * * <p>Note that it is permissible for session to be null, in which case * this method will always return the default visiblity for the field * in question.</p> * * <p>If field is not from an object of the same base as this DBEditObject, * an exception will be thrown.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public boolean canSeeField(DBSession session, DBField field) { // by default, return the field definition's visibility if (field.getFieldDef().base() != this.objectBase) { // "field/object mismatch" throw new IllegalArgumentException(ts.l("canSeeField.mismatch")); } return field.getFieldDef().isVisible(); } /** * <p>Controls whether change information for the field can be safely * logged to the Ganymede log and emailed to the owner notification * list for the field's containing object.</p> * * <p>If okToLogField() returns false for a field, the change to the * field will be mentioned in email and the log, but the actual * values will not be.</p> * * <p>With the exception of PasswordDBFields, you should consider any * field for which okToLogField() returns true to be public data, as * any user able to log into Ganymede may be able to see the data in * the logs. PasswordDBFields contain special logic to prevent the * data they contain from being revealed in logs or email.<p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public boolean okToLogField(DBField field) { return true; } /** * <p>Customization method to verify whether this object type has an inactivation * mechanism.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public boolean canBeInactivated() { return false; } /** * <p>Customization method to verify whether the user has permission * to inactivate a given object. The client's * {@link arlut.csd.ganymede.server.DBSession DBSession} object * will call this per-class method to do an object type- * sensitive check to see if this object feels like being * available for inactivating by the client.</p> * * <p>Note that unlike canRemove(), canInactivate() takes a * DBEditObject instead of a DBObject. This is because inactivating * an object is based on editing the object, and so we have the * GanymedeSession/DBSession classes go ahead and check the object * out for editing before calling us. This serves to force the * session classes to check for write permission before attempting * inactivation.</p> * * <p>Use canBeInactivated() to test for the presence of an inactivation * protocol outside of an edit context if needed.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public boolean canInactivate(DBSession session, DBEditObject object) { return false; } /** * <p>Customization method to verify whether the user has permission * to remove a given object. The client's DBSession object * will call this per-class method to do an object type- * sensitive check to see if this object feels like being * available for removal by the client.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public ReturnVal canRemove(DBSession session, DBObject object) { // our default behavior is that objects that can be inactivated // should not be deleted except by supergash if (this.canBeInactivated() && !session.getGSession().getPermManager().isSuperGash()) { return ReturnVal.failure(); } return null; // success } /** * <p>Customization method to verify whether the user has permission * to clone a given object. The client's {@link * arlut.csd.ganymede.server.GanymedeSession} object will call this * per-class method to do an object type- sensitive check to see if * this object feels like being available for cloning by the * client.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public boolean canClone(DBSession session, DBObject object) { return !object.isEmbedded(); } /** * <p>Customization method to verify whether a specific field * in object should be cloned using the basic field-clone * logic.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public boolean canCloneField(DBSession session, DBObject object, DBField field) { // don't clone the built-in fields, but do clone // anything else return field.getID() > SchemaConstants.FinalSystemField; } /** * <p>Customization method to verify whether the user has permission * to create an instance of this object type. The client's * {@link arlut.csd.ganymede.server.DBSession DBSession} object * will call the canCreate method in the * {@link arlut.csd.ganymede.server.DBObjectBase DBObjectBase} for this object type * to determine whether creation is allowed to the user.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public boolean canCreate(Session session) { if (session != null && (session instanceof GanymedeSession)) { GanymedeSession gSession; /* -- */ gSession = (GanymedeSession) session; return gSession.getPermManager().getPerm(getTypeID(), true).isCreatable(); // *sync* GanymedeSession } // note that we are going ahead and returning false here, as // we assume that the client will always use the local BaseDump // copy and won't generally call us remotely with a remote // interface. return false; } /** * <p>Hook to allow subclasses to grant ownership privileges to a given * object. If this method returns true on a given object, the Ganymede * Permissions system will provide access to the object as owned with * whatever permissions apply to objects owned by the persona active * in gSession.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public boolean grantOwnership(GanymedeSession gSession, DBObject object) { return false; } /** * <p>This method provides a hook to allow custom DBEditObject subclasses to * indicate that the given object is interested in receiving notification * when changes involving it occur, and can provide one or more addresses for * such notification to go to.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public boolean hasEmailTarget(DBObject object) { return false; } /** * <p>This method provides a hook to allow custom DBEditObject subclasses to * return a List of Strings comprising a list of addresses to be * notified above and beyond the normal owner group notification when * the given object is changed in a transaction. Used for letting end-users * be notified of changes to their account, etc.</p> * * <p>If no email targets are present in this object, either a null value * or an empty List may be returned.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public List<String> getEmailTargets(DBObject object) { return null; } /** * <p>This method provides a hook to allow custom DBEditObject * subclasses to return a String containing a URL for an image to * represent this object. Intended to be used for users, primarily.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> */ public String getImageURLForObject(DBObject object) { return null; // no image } /** * <p>This method provides a hook to allow custom DBEditObject * subclasses to react to forthcoming object expiration, during the * execution of the GanymedeWarningTask.</p> * * <p>This method will be called without benefit of an open DBEditSet, * so any email generated will need to make use of the * non-transactional mail methods in the Ganymede.log object.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> * * @return true if the DBEditObject subclass wishes to completely * handle the warning, or false if the default warning transmisssion * logic should also be sent. */ public boolean reactToExpirationWarning(DBObject object, int days) { return false; } /** * <p>This method provides a hook to allow custom DBEditObject * subclasses to react to forthcoming object removal.</p> * * <p>This method will be called without benefit of an open DBEditSet, * so any email generated will need to make use of the * non-transactional mail methods in the Ganymede.log object.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> * * @return true if the DBEditObject subclass wishes to completely * handle the warning, or false if the default warning transmisssion * logic should also be sent. */ public boolean reactToRemovalWarning(DBObject object, int days) { return false; } /** * <p>This method provides a hook that can be used by subclasses of * DBEditObject to return a list of attribute names and attribute * values to include when writing out <invid> elements to a * sync channel during transactional sync'ing.</p> * * <p>The point of this is to allow DBEditObject subclasses to * inject additional data into a transactional sync record so that * external sync channel service code can have enough information to * identify a relationship that was made or broken within the * tranaction.</p> * * <p>The array returned should have an even number of values. The * first value should be an attribute name, the second should be * the value for that attribute name, the third should be another * attribute name, and etc.</p> * * <p>It is an error to return an attribute name that conflicts with * the set pre-defined for use with the <invid> XML element. * This includes <code>type</code>, <code>num</code>, <code>id</code>, * and <code>oid</code>.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> * * @param invid The Invid that is to be written out. * @param sourceObj The object that contains the Invid to be written out. * @param targetObj The object that the invid resolves to. * @param syncChannel The name of the Sync Channel that this Invid is being written to. * @param forceOriginal If true and the foreign sync keys are being * generated in a transactional context (as in the incremental Sync * Channel writes), getForeignSyncKeys() will attempt to resolve the * extra attributes against the original version of the object targeted * by invid. */ public String[] getForeignSyncKeys(Invid invid, DBObject sourceObj, DBObject targetObj, String syncChannel, boolean forceOriginal) { return null; } /** * <p>This method provides a hook that can be used by subclasses of * DBEditObject to return a list of attribute names and attribute * values to include when writing out <invid> elements that * point to them during a sync channel operation. This method * differs from {@link * arlut.csd.ganymede.server.DBEditObject#getForeignSyncKeys(arlut.csd.ganymede.common.Invid, * arlut.csd.ganymede.server.DBObject, * arlut.csd.ganymede.server.DBObject, java.lang.String, boolean)} * in that this method adds extra attributes to <invid> * elements pointing to objects of this kind, where * getForeignSyncKeys() adds extra attributes to <invid> * elements that point from objects of this kind.</p> * * <p>The point of this is to allow DBEditObject subclasses to * inject additional data into a transactional sync record so that * external sync channel service code can have enough information to * identify a relationship that was made or broken within the * transaction. For instance, if you are synchronizing to an LDAP * structure with the XML Sync Channel mechanism, you might want all * of your <invid> elements that point to Users to include a * dn attribute that provides the fully qualified LDAP DN for the * User object.</p> * * <p>The array returned should have an even number of values. The * first value should be an attribute name, the second should be * the value for that attribute name, the third should be another * attribute name, and etc.</p> * * <p>It is an error to return an attribute name that conflicts with * the set pre-defined for use with the <invid> XML element. * This includes <code>type</code>, <code>num</code>, <code>id</code>, * and <code>oid</code>.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * <p><b>*PSEUDOSTATIC*</b></p> * * @param myObj The object that is being targetted by an <invid> element. * @param syncChannel The name of the Sync Channel that the <invid> is being written to. * @param forceOriginal If true and the foreign sync keys are being * generated in a transactional context (as in the incremental Sync * Channel writes), getMySyncKeys() will attempt to resolve the * extra attributes against the original version of any objects resolved during * generation of the extra attributes. */ public String[] getMyExtraInvidAttributes(DBObject myObj, String syncChannel, boolean forceOriginal) { return null; } /* -------------------- editing/creating Customization hooks -------------------- The following block of methods are intended to be subclassed to provide intelligence for the object creation/editing process. */ /** * <p>Initializes a newly created DBEditObject.</p> * * <p>When this method is called, the DBEditObject has been created, * its ownership set, and all fields defined in the controlling * {@link arlut.csd.ganymede.server.DBObjectBase DBObjectBase} have * been instantiated without defined values. If this DBEditObject * is an embedded type, it will have been linked into its parent * object before this method is called.</p> * * <p>This method is responsible for filling in any default values * that can be calculated from the {@link * arlut.csd.ganymede.server.DBSession DBSession} associated with * the editset defined in this DBEditObject.</p> * * <p>If initialization fails for some reason, initializeNewObject() * will return a ReturnVal with an error result.. If the owning * GanymedeSession is not in bulk-loading mode (i.e., * GanymedeSession.enableOversight is true), {@link * arlut.csd.ganymede.server.DBSession#createDBObject(short, * arlut.csd.ganymede.common.Invid, java.util.Vector) * DBSession.createDBObject()} will checkpoint the transaction * before calling this method. If this method returns a failure * code, the calling method will rollback the transaction. This * method has no responsibility for undoing partial initialization, * the checkpoint/rollback logic will take care of that.</p> * * <p>If enableOversight is false, DBSession.createDBObject() will * not checkpoint the transaction status prior to calling * initializeNewObject(), so it is the responsibility of this method * to handle any checkpointing needed.</p> * * <p>This method should be overridden in subclasses.</p> * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public ReturnVal initializeNewObject() { return null; } /** * <p>This method provides a hook that can be used to indicate whether * a field that is defined in this object's field dictionary * should be newly instantiated in this particular object.</p> * * <p>This method does not affect those fields which are actually present * in a previously existing object's record in the * {@link arlut.csd.ganymede.server.DBStore DBStore}. What this method allows * you to do is have a subclass decide whether it wants to instantiate * a potential field (one that is declared in the field dictionary for * this object, but which doesn't happen to be presently defined in * this object) in this particular object.</p> * * <p>A concrete example will help here. The Permissions Object type * (base number SchemaConstants.PermBase) holds a permission * matrix, a descriptive title, and a list of admin personae that hold * those permissions for objects they own.</p> * * <p>There are a few specific instances of SchemaConstants.PermBase * that don't properly need the list of admin personae, as their * object invids are hard-coded into the Ganymede security system, and * their permission matrices are automatically consulted in certain * situations. In order to support this, we're going to want to have * a DBEditObject subclass for managing permission objects. In that * subclass, we'll define instantiateNewField() so that it will return * false if the fieldID corresponds to the admin personae list if the * object's ID is that of one of these special objects. As a result, * when the objects are viewed by an administrator, the admin personae * list will not be seen.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> */ public boolean instantiateNewField(short fieldID) { return getGSession().getPermManager().getPerm(getTypeID(), fieldID, true).isCreatable(); // *sync* GanymedeSession } /** * <p>Hook to allow the cloning of an object. If this object type * supports cloning (which should be very much customized for this * object type.. creation of the ancillary objects, which fields to * clone, etc.), this customization method will actually do the * work.</p> * * <p>This method is called on a newly created object, in order to * clone the state of origObj into it. This method does not * actually create a new object.. that is handled by {@link * arlut.csd.ganymede.server.GanymedeSession#clone_db_object(arlut.csd.ganymede.common.Invid) * clone_db_object()} before this method is called on the newly * created object.</p> * * <p>The default (DBEditObject) implementation of this method will * only clone fields for which {@link * arlut.csd.ganymede.server.DBEditObject#canCloneField(arlut.csd.ganymede.server.DBSession, * arlut.csd.ganymede.server.DBObject, * arlut.csd.ganymede.server.DBField) canCloneField()} returns true, * and which are not connected to a namespace (and thus could not * possibly be cloned, because the values are constrained to be * unique and non-duplicated).</p> * * <p>If one or more fields in the original object are unreadable by * the cloning session, we will provide a list of fields that could * not be cloned due to a lack of read permissions in a dialog in * the ReturnVal. Such a problem will not result in a failure code * being returned, however.. the clone will succeed, but an * informative dialog will be provided to the user.</p> * * <p>To be overridden on necessity in DBEditObject subclasses, but * this method's default logic will probably do what you need it to * do. If you need to make changes, try to chain your subclassed * method to this one via super.cloneFromObject().</p> * * @param session The DBSession that the new object is to be created in * @param origObj The object we are cloning * @param local If true, fields that have choice lists will not be checked against * those choice lists and read permissions for each field will not be consulted. * The canCloneField() method will still be consulted, however. * * @return A standard ReturnVal status object. May be null on success, or * else may carry a dialog with information on problems and a success flag. */ public ReturnVal cloneFromObject(DBSession session, DBObject origObj, boolean local) { ReturnVal retVal; Vector<DBField> origFields; DBField newField; boolean problem = false; StringBuilder resultBuf = new StringBuilder(); /* -- */ if ((origObj.getTypeID() != getTypeID())) { // "Clone Error" // "Can''t clone an object of the wrong type. This is an internal error." return Ganymede.createErrorDialog(this.getGSession(), ts.l("cloneFromObject.error"), ts.l("cloneFromObject.typeError")); } origFields = origObj.getFieldVector(true); // don't clone system fields for (DBField origField: origFields) { if (canCloneField(session, origObj, origField)) { newField = getField(origField.getID()); // if we already initialized this field when we were // constructed, don't copy over a value onto this field. // this is to allow initializeNewObject() to handle // object-unique values if (newField.isDefined()) { continue; } // if the field is guarded by a unique value namespace, don't // attempt to clone anything to it if (newField.getNameSpace() != null) { continue; } // if the field is an invid editinplace field, don't try // to clone the actual invid pointers to contained // objects. if (newField.isEditInPlace()) { continue; } // if the field is a password field, don't try to clone it if (newField instanceof PasswordDBField) { continue; } // if the field is an invid field whose target is a scalar // invid field, then this field has an association which // is a one-to-one or many-to-one, and shouldn't be // cloned, lest we disrupt the previous association if (newField instanceof InvidDBField) { InvidDBField iField = (InvidDBField) newField; DBObjectBaseField targetDef = iField.getTargetFieldDef(); if (targetDef != null && !targetDef.isArray()) { continue; } } // and do the thing // copyFieldTo() checks read permissions on the // original object's field, and will return an error // dialog if the user doesn't have permission to read // the field. If we have a problem, we'll return a // dialog describing the fields that could not be // cloned, but we won't fail the operation. retVal = origField.copyFieldTo(newField, local); if (retVal != null && retVal.getDialog() != null) { if (resultBuf.length() != 0) { resultBuf.append("\n\n"); } resultBuf.append(retVal.getDialog().getText()); problem = true; } } } // we're returning a successful result if we've gotten here, but // 'normal operations' might be false if some fields could not be // cloned due to read permissions. retVal = new ReturnVal(true, !problem); if (problem) { // "Possible Clone Problems" retVal.setDialog(new JDialogBuff(ts.l("cloneFromObject.possibleError"), resultBuf.toString(), ts.l("global.buttonOK"), null, "ok.gif")); } return retVal; } /** * <p>This method is the hook that DBEditObject subclasses use to interpose * {@link arlut.csd.ganymede.server.GanymediatorWizard wizards} when a field's * value is being changed.</p> * * <p>Whenever a field is changed in this object, this method will be * called with details about the change. This method can refuse to * perform the operation, it can make changes to other objects in * the database in response to the requested operation, or it can * choose to allow the operation to continue as requested.</p> * * <p>In the latter two cases, the wizardHook code may specify a list * of fields and/or objects that the client may need to update in * order to maintain a consistent view of the database.</p> * * <p>If server-local code has called * {@link arlut.csd.ganymede.server.GanymedeSession#enableOversight(boolean) * enableOversight(false)}, * this method will never be * called. This mode of operation is intended only for initial * bulk-loading of the database.</p> * * <p>This method may also be bypassed when server-side code uses * setValueLocal() and the like to make changes in the database.</p> * * <p>This method is called before the finalize*() methods.. the finalize*() * methods is where last minute cascading changes should be performed.. * Note as well that wizardHook() is called before the namespace checking * for the proposed value is performed, while the finalize*() methods are * called after the namespace checking.</p> * * <p>The operation parameter will be a small integer, and should hold one of the * following values:</p> * * <dl> * <dt>1 - SETVAL</dt> * <dd>This operation is used whenever a simple scalar field is having * it's value set. param1 will be the value being placed into the field.</dd> * <dt>2 - SETELEMENT</dt> * <dd>This operation is used whenever a value in a vector field is being * set. param1 will be an Integer holding the element index, and * param2 will be the value being set.</dd> * <dt>3 - ADDELEMENT</dt> * <dd>This operation is used whenever a value is being added to the * end of a vector field. param1 will be the value being added.</dd> * <dt>4 - DELELEMENT</dt> * <dd>This operation is used whenever a value in a vector field is being * deleted. param1 will be an Integer holding the element index.</dd> * <dt>5 - ADDELEMENTS</dt> * <dd>This operation is used whenever a set of elements is being * added to a vector field en masse. param1 will be a Vector containing * the values that are being added.</dd> * <dt>6 - DELELEMENTS</dt> * <dd>This operation is used whenever a set of elements is being * deleted from a vector field en masse. param1 will be a Vector containing * the values that are being deleted.</dd> * <dt>7 - SETPASSPLAIN</dt> * <dd>This operation is used when a password field is having its password * set using a plaintext source. param1 will be a String containing the * submitted password, or null if the password is being cleared.</dd> * <dt>8 - SETPASSCRYPT</dt> * <dd>This operation is used when a password field is having its password * set using a UNIX crypt() hashed source. param1 will be a String containing the * submitted hashed password, or null if the password is being cleared.</dd> * <dt>9 - SETPASSMD5</dt> * <dd>This operation is used when a password field is having its password * set using an md5Ccrypt() hashed source. param1 will be a String containing the * submitted hashed password, or null if the password is being cleared.</dd> * <dt>10 - SETPASSWINHASHES</dt> * <dd>This operation is used when a password field is having its password * set using Windows style password hashes. param1 will be the password in * LANMAN hash form, param2 will be the password in NT Unicode MD4 hash * form. Either or both of param1 and param2 may be null.</dd> * <dt>11 - SETPASSAPACHEMD5</dt> * <dd>This operation is used when a password field is having its * password set using the Apache variant of the md5crypt algorithm. * param1 will be the password in Apache md5crypt hash form, or null * if the password hash is being cleared. param2 will be null.</dd> * <dt>12 - SETPASSSSHA</dt> * <dd>This operation is used when a password field is having its * password set using the OpenLDAP-style SSHA password hash. param1 * will be the password in SSHA form, or null if the password is * being cleared. param2 will be null.</dd> * <dt>13 - SETPASS_SHAUNIXCRYPT</dt> * <dd>This operation is used when a password field is having its * password set using Ulrich Drepper's SHA256 or SHA512 Unix Crypt * algorithms. param1 will be the password in SHA Unix Crypt form, * or null if the password is being cleared. param2 will be * null.</dd> * <dt>14 - SETPASS_BCRYPT</dt> * <dd>This operation is used when a password field is having its * password set using the OpenBSD-style BCrypt password hash. param1 * will be the password in BCrypt form, or null if the password is * being cleared. param2 will be null.</dd> * </dl> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * @return null if the operation is approved without comment, or a * ReturnVal object indicating success or failure, objects and * fields to be rescanned by the client, and a doNormalProcessing * flag that will indicate to the field code whether or not the * operation should continue to completion using the field's * standard logic. <b>It is very important that wizardHook return a * new ReturnVal(true, true) if the wizardHook wishes to simply * specify rescan information while having the field perform its * standard operation.</b> wizardHook() may return new * ReturnVal(true, false) if the wizardHook performs the operation * (or a logically related operation) itself. The same holds true * for the respond() method in GanymediatorWizard subclasses. */ public ReturnVal wizardHook(DBField field, int operation, Object param1, Object param2) { return null; // by default, we just ok whatever } /** * <p>Hook to have this object create a new embedded object * in the given field.</p> * * <p>This method now has the appropriate default logic for creating * embedded objects with the user's permissions, but this method may * still be overridden to do customization, if needed.</p> * * <p>XXX: It is essential that createNewEmbeddedObject() use * GanymedeSession.create_db_object() in order to check permissions * and to export the created embedded object for the client's use, if * necessary, whatever other customizations a subclass might choose to * do. We may want to make this method final.</p> * * @return A ReturnVal indicating success or failure. If the * embedded object was created successfully, the getInvid() method * on the returned ReturnVal will provide the Invid of the created * object. */ public ReturnVal createNewEmbeddedObject(InvidDBField field) throws NotLoggedInException { if (getGSession() == null) { // "Error in custom code on server. createNewEmbeddedObject() called without a valid GanymedeSession" return Ganymede.createErrorDialog(this.getGSession(), ts.l("global.serverError"), ts.l("createNewEmbeddedObject.badSession")); } DBObjectBaseField fieldDef = field.getFieldDef(); if (!fieldDef.isEditInPlace()) { // "Error in server, DBEditObject.createNewEmbeddedObject() called on non-embedded object" throw new RuntimeException(ts.l("createNewEmbeddedObject.embeddedError")); } // we can't create an embedded object unless we know what type to // create if (fieldDef.getTargetBase() == -1) { // "Error in custom code on server. createNewEmbeddedObject() called without a valid target" return Ganymede.createErrorDialog(this.getGSession(), ts.l("global.serverError"), ts.l("createNewEmbeddedObject.badTarget")); } // we use GanymedeSession to check permissions to create the target. ReturnVal retVal = getGSession().create_db_object(fieldDef.getTargetBase(), true); if (retVal == null) { // "DBEditObject.createNewEmbeddedObject could not get a useful result from create_db_object" return Ganymede.createErrorDialog(this.getGSession(), ts.l("global.serverError"), ts.l("createNewEmbeddedObject.badCreate")); } return retVal; } /** * <p>Provides a hook that can be used to approve, disapprove, * and/or transform any values to be set in any field in this * object.</p> * * <p>verifyNewValue can be used to canonicalize a * submitted value. The verifyNewValue method may call * {@link arlut.csd.ganymede.common.ReturnVal#setTransformedValueObject(java.lang.Object, arlut.csd.ganymede.common.Invid, short) setTransformedValue()} * on the ReturnVal returned in order to substitute a new value for * the provided value prior to any other processing on the server.</p> * * <p>This method is called before any NameSpace checking is done, before the * {@link arlut.csd.ganymede.server.DBEditObject#wizardHook(arlut.csd.ganymede.server.DBField,int,java.lang.Object,java.lang.Object) wizardHook()} * method, and before the appropriate * {@link arlut.csd.ganymede.server.DBEditObject#finalizeSetValue(arlut.csd.ganymede.server.DBField, Object) finalizeSetValue()}, * {@link arlut.csd.ganymede.server.DBEditObject#finalizeSetElement(arlut.csd.ganymede.server.DBField, int, Object) finalizeSetElement()}, * {@link arlut.csd.ganymede.server.DBEditObject#finalizeAddElement(arlut.csd.ganymede.server.DBField, java.lang.Object) finalizeAddElement()}, * or {@link arlut.csd.ganymede.server.DBEditObject#finalizeAddElements(arlut.csd.ganymede.server.DBField, java.util.Vector) finalizeAddElements()} * method is called.</p> * * @param field The DBField contained within this object whose value * is being changed * @param value The value that is being proposed to go into field. * * @return A ReturnVal indicating success or failure. May be simply * 'null' to indicate success if no feedback need be provided. If * {@link arlut.csd.ganymede.common.ReturnVal#hasTransformedValue() hasTransformedValue()} * returns true when callled on the returned ReturnVal, the value * returned by {@link arlut.csd.ganymede.common.ReturnVal#getTransformedValueObject() getTransformedValueObject()} * will be used for all further processing in the server, and will * be the value actually saved in the DBStore. */ public ReturnVal verifyNewValue(DBField field, Object value) { return null; } // **** // // The following methods are here to allow our DBEditObject // to be involved in the processing of a particular // vector operation on a field in this object.. otherwise // we'd have to subclass our fields for any processing // that would need to be done in response to an operation.. // // **** /** * <p>This method allows the DBEditObject to have executive approval of * any vector delete operation, and to take any special actions in * reaction to the delete.. if this method returns null or a success * code in its ReturnVal, the {@link arlut.csd.ganymede.server.DBField DBField} * that called us is guaranteed to proceed to * make the change to its vector. If this method returns a * non-success code in its ReturnVal, the DBField that called us * will not make the change, and the field will be left * unchanged.</p> * * <p>The <field> parameter identifies the field that is requesting * approval for item deletion, and the <index> parameter identifies * the element number that is to be deleted.</p> * * <p>The DBField that called us will take care of all standard * checks on the operation (including vector bounds, etc.) before * calling this method. Under normal circumstances, we won't need * to do anything here.</p> * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public ReturnVal finalizeDeleteElement(DBField field, int index) { return null; } /** * <p>This method allows the DBEditObject to have executive approval * of any vector-vector removal operation, and to take any special * actions in reaction to the removal.. if this method returns null * or a success code in its ReturnVal, the DBField that called us is * guaranteed to proceed to make the change to its vector. If this * method returns a non-success code in its ReturnVal, the DBField * that called us will not make the change, and the field will be * left unchanged.</p> * * <p>The <field> parameter identifies the field that is * requesting approval for item deletion, and the * <valuesToDelete> parameter contains a list of elements to * be deleted.</p> * * <p>The DBField that called us will take care of all standard * checks on the operation (including vector bounds, etc.) before * calling this method. Under standard circumstances, we won't need * to do anything here.</p> * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public ReturnVal finalizeDeleteElements(DBField field, Vector valuesToDelete) { return null; } /** * <p>This method allows the DBEditObject to have executive approval * of any vector add operation, and to take any special actions in * reaction to the add.. if this method returns null or a success * code in its ReturnVal, the DBField that called us is guaranteed * to proceed to make the change to its vector. If this method * returns a non-success code in its ReturnVal, the DBField that * called us will not make the change, and the field will be left * unchanged.</p> * * <p>The <field> parameter identifies the field that is * requesting approval for item deletion, and the <value> * parameter carries the value to be added.</p> * * <p>The DBField that called us will take care of all standard * checks on the operation (including vector bounds, etc.) before * calling this method. Under normal circumstances, we won't need * to do anything here.</p> * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public ReturnVal finalizeAddElement(DBField field, Object value) { return null; } /** * <p>This method allows the DBEditObject to have executive approval * of any vector-vector add operation, and to take any special * actions in reaction to the add.. if this method returns null or a * success code in its ReturnVal, the DBField that called us is * guaranteed to proceed to make the change to its vector. If this * method returns a non-success code in its ReturnVal, the DBField * that called us will not make the change, and the field will be * left unchanged.</p> * * <p>The <field> parameter identifies the field that is * requesting approval for item deletion, and the <submittedValues> * parameter carries the values to be added.</p> * * <p>The DBField that called us will take care of all standard * checks on the operation (including vector bounds, etc.) before * calling this method. Under normal circumstances, we won't need * to do anything here.</p> * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public ReturnVal finalizeAddElements(DBField field, Vector submittedValues) { return null; } /** * <p>This method allows the DBEditObject to have executive approval * of any vector set operation, and to take any special actions in * reaction to the set.. if this method returns null or a success * code in its ReturnVal, the DBField that called us is guaranteed * to proceed to make the change to its vector. If this method * returns a non-success code in its ReturnVal, the DBField that * called us will not make the change, and the field will be left * unchanged.</p> * * <p>The <field> parameter identifies the field that is * requesting approval for item deletion, the <index> * parameter identifies which element in the given vector field is * to be set, and the <value> parameter carries the proposed * new value.</p> * * <p>The DBField that called us will take care of all standard * checks on the operation (including vector bounds, etc.) before * calling this method. Under normal circumstances, we won't need * to do anything here.</p> * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public ReturnVal finalizeSetElement(DBField field, int index, Object value) { return null; } /** * <p>This method allows the DBEditObject to have executive approval * of any scalar set operation, and to take any special actions in * reaction to the set. When a scalar field has its value set, it * will call its owners finalizeSetValue() method, passing itself as * the <field> parameter, and passing the new value to be * approved as the <value> parameter. A Ganymede customizer * who creates custom subclasses of the DBEditObject class can * override the finalizeSetValue() method and write his own logic * to examine any change and either approve or reject the change.</p> * * <p>A custom finalizeSetValue() method will typically need to * examine the field parameter to see which field is being changed, * and then do the appropriate checking based on the value * parameter. The finalizeSetValue() method can call the normal * this.getFieldValueLocal() type calls to examine the current state * of the object, if such information is necessary to make * appropriate decisions.</p> * * <p>If finalizeSetValue() returns null or a ReturnVal object with * a positive success value, the DBField that called us is * guaranteed to proceed to make the change to its value. If this * method returns a non-success code in its ReturnVal, as with the * result of a call to Ganymede.createErrorDialog(), the DBField * that called us will not make the change, and the field will be * left unchanged. Any error dialog returned from finalizeSetValue() * will be passed to the user.</p> * * <p>The DBField that called us will take care of all standard * checks on the operation (including a call to our own * verifyNewValue() method before calling this method. Under normal * circumstances, we won't need to do anything here. * finalizeSetValue() is useful when you need to do unusually * involved checks, and for when you want a chance to trigger other * changes in response to a particular field's value being * changed.</p> * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public ReturnVal finalizeSetValue(DBField field, Object value) { return null; } /** * <p>This method returns true if field1 should not show any choices * that are currently selected in field2, where both field1 and * field2 are fields in this object.</p> * * <p>The purpose of this method is to allow mutual exclusion between * a pair of fields with mandatory choices.</p> * * To be overridden on necessity in DBEditObject subclasses. */ public boolean excludeSelected(db_field field1, db_field field2) { return false; } /** * <p>This method returns a key that can be used by the client * to cache the value returned by choices(). If the client * already has the key cached on the client side, it * can provide the choice list from its cache rather than * calling choices() on this object again.</p> * * <p>The default logic in this method is designed to cause the client * to cache choice lists for invid fields in the 'all objects of * invid target type' cache bucket. If your InvidDBField needs to * provide a restricted subset of objects of the targeted type as * the choice list, you'll need to override this method to either * return null (to turn off choice list caching), or generate some * kind of unique key that won't collide with the Short objects used * to represent the default object list caches.</p> * * <p>See also the {@link * arlut.csd.ganymede.server.DBEditObject#choiceListHasExceptions(arlut.csd.ganymede.server.DBField)} * hook, which controls whether or not the default logic will * encourage the client to cache a given InvidDBField's choice list.</p> * * <p>If there is no caching key, this method will return null.</p> */ public Object obtainChoicesKey(DBField field) { // by default, we return a Short containing the base // id for the field's target if ((field instanceof InvidDBField) && !field.isEditInPlace()) { DBObjectBaseField fieldDef; short baseId; /* -- */ fieldDef = field.getFieldDef(); baseId = fieldDef.getTargetBase(); if (baseId < 0) { return null; } return Short.valueOf(baseId); } return null; } /** * <p>This method provides a hook that can be used to generate * choice lists for invid and string fields that provide * such. String and Invid DBFields will call their owner's * obtainChoiceList() method to get a list of valid choices.</p> * * <p>This method will provide a reasonable default for targetted * invid fields, filtered by the GanymedeSession's * visibilityFilterInvids list.</p> * * <p>NOTE: This method does not need to be synchronized. Making this * synchronized can lead to DBEditObject/DBSession nested monitor * deadlocks.</p> */ public QueryResult obtainChoiceList(String fieldName) throws NotLoggedInException { return obtainChoiceList(getField(fieldName)); } /** * <p>This method provides a hook that can be used to generate * choice lists for invid and string fields that provide * such. String and Invid DBFields will call their owner's * obtainChoiceList() method to get a list of valid choices.</p> * * <p>This method will provide a reasonable default for targetted * invid fields, filtered by the GanymedeSession's * visibilityFilterInvids list.</p> * * <p>NOTE: This method does not need to be synchronized. Making this * synchronized can lead to DBEditObject/DBSession nested monitor * deadlocks.</p> */ public QueryResult obtainChoiceList(short fieldID) throws NotLoggedInException { return obtainChoiceList(getField(fieldID)); } /** * <p>This method provides a hook that can be used to generate * choice lists for invid and string fields that provide * such. String and Invid DBFields will call their owner's * obtainChoiceList() method to get a list of valid choices.</p> * * <p>This method will provide a reasonable default for targetted * invid fields, filtered by the GanymedeSession's * visibilityFilterInvids list.</p> * * <p>NOTE: This method does not need to be synchronized. Making this * synchronized can lead to DBEditObject/DBSession nested monitor * deadlocks.</p> */ public QueryResult obtainChoiceList(DBField field) throws NotLoggedInException { if (field instanceof InvidDBField && !field.isEditInPlace() && field.isEditable()) { DBObjectBaseField fieldDef; short baseId; /* -- */ fieldDef = field.getFieldDef(); baseId = fieldDef.getTargetBase(); if (baseId < 0) { // Ganymede.debug("DBEditObject: Returning null 2 for choiceList for field: " + field.getName()); return null; } // and we want to return a list of choices.. can use the regular // query output here QueryNode root; // if we are pointing to objects of our own type, we don't want ourselves to be // a valid choice by default.. (DBEditObject subclasses can override this, of course) // XXX // // note: if we are omitting ourself from a choice list, we // sure shouldn't be cached by the client. Right now, nothing // ensures this. // // XXX if (baseId == getTypeID()) { root = new QueryNotNode(new QueryDataNode((short) -2, QueryDataNode.EQUALS, getInvid())); } else { root = null; } boolean editOnly = !choiceListHasExceptions(field); Query myQuery = new Query(baseId, root, editOnly); myQuery.setFiltered(false); // be sure not to filter the query return getDBSession().getGSession().query(myQuery, this); } // Ganymede.debug("DBEditObject: Returning null for choiceList for field: " + field.getName()); return null; } /** * <p>This method is used to tell the client whether the list of options it gets * back for a field can be taken out of the cache. If this method returns * true, that means that some of the results that obtainChoiceList() will * return will include items that normally wouldn't be availble to the * client, but are in this case because of the anonymousLinkOK() results.</p> * * <p>This is kind of wacked-out random stuff. It's basically a way of * allowing DBEditObject to get involved in the decision as to whether an * {@link arlut.csd.ganymede.server.InvidDBField InvidDBField}'s * {@link arlut.csd.ganymede.server.InvidDBField#choicesKey() choicesKey()}'s * method should disallow client-side caching for the field's choice list.</p> * * <p>To be overridden on necessity in DBEditObject subclasses, though the default * logic here tries to be smart about the typical cases.</p> */ public boolean choiceListHasExceptions(DBField field) { if (!(field instanceof InvidDBField)) { // "field not an InvidDBField" throw new IllegalArgumentException(ts.l("choiceListHasExceptions.badType")); } // -- DBObjectBaseField fieldDef; short baseId; short targetField; /* -- */ fieldDef = field.getFieldDef(); baseId = fieldDef.getTargetBase(); if (fieldDef.isSymmetric()) { targetField = fieldDef.getTargetField(); } else { targetField = SchemaConstants.BackLinksField; } DBObjectBase targetBase = Ganymede.db.getObjectBase(baseId); return targetBase.getObjectHook().anonymousLinkOK(null, targetField, this, field.getID(), this.getGSession()); } /** * <p>This method provides a hook that a DBEditObject subclass * can use to indicate whether a given field can only * choose from a choice provided by obtainChoiceList()</p> * * <p>To be overridden on necessity in DBEditObject subclasses, * particularly if you have a StringDBField that you want to force * to pick from the list of choices provided by your DBEditObject * subclass' obtainChoiceList() method.</p> */ public boolean mustChoose(DBField field) { // by default, we assume that InvidDBField's are always // must choose. if (field instanceof InvidDBField) { return true; } return false; } /** * <p>This method provides a hook that a DBEditObject subclass can use * to determine whether it is permissible to enter IPv6 address in a * particular (IP) DBField. The default assumption is that IPv6 * addresses are not accepted.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> */ public boolean isIPv6OK(DBField field) { return false; } /** * <p>This method provides a hook that a DBEditObject subclass * can use to indicate that a given * {@link arlut.csd.ganymede.server.DateDBField DateDBField} has a restricted * range of possibilities.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> */ public boolean isDateLimited(DBField field) { if (getGSession() != null && getGSession().enableWizards && ((field.getID() == SchemaConstants.ExpirationField) || (field.getID() == SchemaConstants.RemovalField))) { return true; // no values in the past, thanks } return false; } /** * <p>This method is used to specify the earliest acceptable date * for the specified {@link arlut.csd.ganymede.server.DateDBField DateDBField}.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> */ public Date minDate(DBField field) { if (getGSession() != null && getGSession().enableWizards && ((field.getID() == SchemaConstants.ExpirationField) || (field.getID() == SchemaConstants.RemovalField))) { return new Date(); // no values in the past, thanks } return new Date(Long.MIN_VALUE); } /** * <p>This method is used to specify the latest acceptable date * for the specified {@link arlut.csd.ganymede.server.DateDBField DateDBField}.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> */ public Date maxDate(DBField field) { return new Date(Long.MAX_VALUE); } /** * <p>This method provides a hook that a DBEditObject subclass * can use to indicate that a given * {@link arlut.csd.ganymede.server.NumericDBField NumericDBField} * has a restricted range of possibilities.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> */ public boolean isIntLimited(DBField field) { return false; } /** * <p>This method is used to specify the minimum acceptable value * for the specified * {@link arlut.csd.ganymede.server.NumericDBField NumericDBField}.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> */ public int minInt(DBField field) { return Integer.MIN_VALUE; } /** * <p>This method is used to specify the maximum acceptable value * for the specified * {@link arlut.csd.ganymede.server.NumericDBField NumericDBField}.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> */ public int maxInt(DBField field) { return Integer.MAX_VALUE; } /** * <p>This method provides a hook that a DBEditObject subclass * can use to indicate that a given * {@link arlut.csd.ganymede.server.FloatDBField FloatDBField} * has a restricted range of possibilities.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> */ public boolean isFloatLimited(DBField field) { return false; } /** * <p>This method is used to specify the minimum acceptable value * for the specified * {@link arlut.csd.ganymede.server.FloatDBField FloatDBField}.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> */ public double minFloat(DBField field) { return Double.MIN_VALUE; } /** * <p>This method is used to specify the maximum acceptable value * for the specified * {@link arlut.csd.ganymede.server.FloatDBField FloatDBField}.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> */ public double maxFloat(DBField field) { return Double.MAX_VALUE; } /** * <p>This method handles inactivation logic for this object type. * A DBEditObject must first be checked out for editing, then the * inactivate() method can then be called on the object to put the * object into inactive mode. inactivate() will set the object's * removal date and fix up any other state information to reflect * the object's inactive status.</p> * * <p>inactive() is designed to run synchronously with the user's * request for inactivation. It can return a wizard reference in * the ReturnVal object returned, to guide the user through a set of * interactive dialogs to inactive the object.</p> * * <p>The inactive() method can cause other objects to be deleted, * can cause strings to be removed from fields in other objects, * whatever.</p> * * <p>If inactivate() returns a ReturnVal that has its success flag * set to false and does not include a JDialogBuff for further * interaction with the user, then DBSEssion.inactivateDBObject() * method will rollback any changes made by this method.</p> * * <p>If inactivate() returns a success value, we expect that the * object will have a removal date set.</p> * * <p>IMPORTANT NOTE 1: This method is intended to be called by the * DBSession.inactivateDBObject() method, which establishes a * checkpoint before calling inactivate. If this method is not * called by DBSession.inactivateDBObject(), you need to push a * checkpoint with the key 'inactivate'+label, where label is the * returned name of this object.</p> * * <p>IMPORTANT NOTE 2: If a custom object's inactivate() logic * decides to enter into a wizard interaction with the user, that * logic is responsible for calling finalizeInactivate() with a * boolean indicating ultimate success of the operation.</p> * * <p>Finally, it is up to commitPhase1() and commitPhase2() to * handle any external actions related to object inactivation when * the transaction is committed..</p> * * @see #commitPhase1() * @see #commitPhase2() * * @param ckp_label The checkpoint label which should be popped or * rolledback on necessity by the custom inactivate method. * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public ReturnVal inactivate(String ckp_label) { // "DBEditObject.inactivate() Error" // "This object type has not been configured to allow inactivation." return Ganymede.createErrorDialog(this.getGSession(), ts.l("inactivate.error"), ts.l("inactivate.errorTxt")); } /** * <p>This method is to be called by the custom DBEditObject inactivate() * logic when the inactivation is performed so that logging can be * done.</p> * * <p>If inactivation of an object causes the label to be null, this * won't work as well as we'd really like.</p> */ public final void finalizeInactivate(boolean success, String ckp_label) { if (success) { Object val = getFieldValueLocal(SchemaConstants.RemovalField); if (val != null) { Vector<Invid> invids = new Vector<Invid>(); invids.add(this.getInvid()); // "{0} {1} has been inactivated.\n\nThe object is due to be removed from the database at {2}.\n\n" editset.logEvent(new DBLogEvent("inactivateobject", ts.l("finalizeInactivate.removeSet", getTypeName(), getLabel(), getFieldValueLocal(SchemaConstants.RemovalField).toString()), getGSession().getPermManager().getResponsibleInvid(), getGSession().getPermManager().getIdentity(), invids, getEmailTargets(this))); } else { Vector<Invid> invids = new Vector<Invid>(); invids.add(this.getInvid()); // "{0} {1} has been inactivated.\n\nThe object has no removal date set.\n\n" editset.logEvent(new DBLogEvent("inactivateobject", ts.l("finalizeInactivate.noRemove", getTypeName(), getLabel()), getGSession().getPermManager().getResponsibleInvid(), getGSession().getPermManager().getIdentity(), invids, getEmailTargets(this))); } editset.popCheckpoint(ckp_label); } else { editset.rollback(ckp_label); } } /** * <p>This method handles reactivation logic for this object type. A * DBEditObject must first be checked out for editing, then the * reactivate() method can then be called on the object to make the * object active again. reactivate() will clear the object's * removal date and fix up any other state information to reflect * the object's reactive status.</p> * * <p>reactive() is designed to run synchronously with the user's * request for inactivation. It can return a wizard reference * in the ReturnVal object returned, to guide the user through * a set of interactive dialogs to reactive the object.</p> * * <p>If reactivate() returns a ReturnVal that has its success flag set to false * and does not include a {@link arlut.csd.JDialog.JDialogBuff JDialogBuff} * for further interaction with the * user, then * {@link arlut.csd.ganymede.server.DBSession#inactivateDBObject(arlut.csd.ganymede.server.DBEditObject) inactivateDBObject()} * method will rollback any changes made by this method.</p> * * <p>IMPORTANT NOTE: If a custom object's inactivate() logic decides * to enter into a wizard interaction with the user, that logic is * responsible for calling finalizeInactivate() with a boolean * indicating ultimate success of the operation.</p> * * <p>Finally, it is up to commitPhase1() and commitPhase2() to handle * any external actions related to object reactivation when * the transaction is committed..</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * @see #commitPhase1() * @see #commitPhase2() * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public ReturnVal reactivate(String ckp_label) { if (isInactivated()) { // by default, we'll just clear the removal field setFieldValueLocal(SchemaConstants.RemovalField, null); return null; // success } // "DBEditObject.reactivate() error" // "I can''t reactivate this object. It was not inactivated." return Ganymede.createErrorDialog(this.getGSession(), ts.l("reactivate.error"), ts.l("reactivate.errorTxt")); } /** * This method is to be called by the custom DBEditObject reactivate() * logic when the reactivation is performed so that logging can be * done. */ protected final void finalizeReactivate(boolean success, String ckp_label) { if (success) { Vector<Invid> invids = new Vector<Invid>(); invids.add(this.getInvid()); // "{0} {1} has been reactivated.\n\n" editset.logEvent(new DBLogEvent("reactivateobject", ts.l("finalizeReactivate.message", getTypeName(), getLabel()), getGSession().getPermManager().getResponsibleInvid(), getGSession().getPermManager().getIdentity(), invids, getEmailTargets(this))); editset.popCheckpoint(ckp_label); } else { editset.rollback(ckp_label); // see DBSession.reactivateDBObject() for checkpoint } } /** * <p>This method handles removal logic for this object type. This method * will be called immediately from DBSession.deleteDBObject().</p> * * <p>The remove() method can cause other objects to be deleted, can cause * strings to be removed from fields in other objects, whatever.</p> * * <p>If remove() returns a ReturnVal that has its success flag set to false * and does not include a JDialogBuff for further interaction with the * user, the DBSession.deleteDBObject() method will roll back any changes * made by this method.</p> * * <p>remove() is intended for subclassing, whereas finalizeRemove() is * not. finalizeRemove() provides the standard logic for wiping out * fields and what not to cause the object to be unlinked from * other objects.</p> * * <p>IMPORTANT NOTE: If a custom object's remove() logic decides to * enter into a wizard interaction with the user, that logic is * responsible for calling finalizeRemove() on the object when * it is determined that the object really should be removed, * with a boolean indicating whether success was had.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public ReturnVal remove() { return null; } /** * <p>This method handles Ganymede-internal deletion logic for this * object type. finalizeRemove() is responsible for dissolving any * invid inter-object references in particular.</p> * * <p>It is up to commitPhase1() and commitPhase2() to handle * any external actions related to object removal when * the transaction is committed..</p> * * <p>finalizeremove() returns a ReturnVal indicating whether the * internal removal bookkeeping was successful. A failure result * will cause the DBSession to rollback the transaction to the state * prior to any removal actions for this object were * attempted.</p> * * <p>remove() is intended for subclassing, whereas finalizeRemove() is * not. finalizeRemove() provides the standard logic for wiping out * fields and what not to cause the object to be unlinked from * other objects.</p> * * @param success If true, finalizeRemove() will clear all fields, * thereby unlinking invid fields and relinquishing namespace claims. * If false, finalizeRemove() will rollback to the state the system * was in before DBSession.deleteDBObject() was entered. * * @see #commitPhase1() * @see #commitPhase2() * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public final synchronized ReturnVal finalizeRemove(boolean success, String ckp_label) { ReturnVal retVal = null; String label = getLabel(); // remember the label before we clear it /* -- */ if (!success) { editset.rollback(ckp_label); // *sync* // "Object Removal Error" // "Could not delete object {0}. Custom code in the server rejected this operation." return Ganymede.createErrorDialog(this.getGSession(), ts.l("finalizeRemove.error"), ts.l("finalizeRemove.errorTxt", label)); } // we want to delete / null out all fields.. this will take care // of invid links, embedded objects, and namespace allocations. // set the deleting flag to true so that our subclasses won't // freak about values being set to null. this.deleting = true; try { // get a sync'ed snapshot of this object's fields Vector<DBField> fieldVect = getFieldVect(); for (DBField memberField: fieldVect) { // we can't clear field 0 (the owner/container field) yet, // since we need that for permissions verifications for // other fields if (memberField.getID() == 0) { continue; } if (memberField.isVector()) { while (memberField.size() > 0) { try { // if this is an edit-in-place InvidDBField, // deleteElement() will convert this request into // a deletion of the embedded object if necessary retVal = ReturnVal.merge(retVal, memberField.deleteElement(0)); // *sync* if (!ReturnVal.didSucceed(retVal)) { editset.rollback(ckp_label); // *sync* if (retVal.getDialog() != null) { return retVal; } // "Server: Error in DBEditObject.finalizeRemove()" // "Custom code disapproved of deleting element from field {0}." return Ganymede.createErrorDialog(this.getGSession(), ts.l("finalizeRemove.myError"), ts.l("finalizeRemove.badDelete", memberField.getName())); } } catch (GanyPermissionsException ex) { editset.rollback(ckp_label); // *sync* // "Server: Error in DBEditObject.finalizeRemove()" // "Permissions violation during attempted deletion of element from field {0}." return Ganymede.createErrorDialog(this.getGSession(), ts.l("finalizeRemove.myError"), ts.l("finalizeRemove.badDeletePerm", memberField.getName())); } } } else { // permission and field option matrices, along with // passwords, don't allow us to call set value // directly. We're mainly concerned with invid's (for // linking), i.p. addresses and strings (for the // namespace) here anyway. if (memberField.getType() != PERMISSIONMATRIX && memberField.getType() != PASSWORD && memberField.getType() != FIELDOPTIONS) { retVal = ReturnVal.merge(retVal, memberField.setValueLocal(null)); // *sync* if (!ReturnVal.didSucceed(retVal)) { editset.rollback(ckp_label); // *sync* if (retVal.getDialog() != null) { return retVal; } // "Server: Error in DBEditObject.finalizeRemove()" // "Custom code disapproved of clearing the value held in field {0}." return Ganymede.createErrorDialog(this.getGSession(), ts.l("finalizeRemove.myError"), ts.l("finalizeRemove.badScalarClear", memberField.getName())); } } else { // catchall for permission matrix, field options, // and password fields, which do this their own // way. try { memberField.setUndefined(true); } catch (GanyPermissionsException ex) { throw new RuntimeException(ex); // should never happen } } } } // ok, we've cleared all fields but field 0.. clear that to finish up. DBField field = getField((short) 0); if (field != null) { if (field.isVector()) { // if we're deleting elements out of vector field 0 (the list // of owner groups), we'll want to deleteElementLocal.. this // will simplify things and will prevent us from losing permission // to write to this field in midstream (although the new DBField // permCache would actually obviate this problem as well). while (field.size() > 0) { retVal = ReturnVal.merge(retVal, field.deleteElementLocal(0)); // *sync* if (!ReturnVal.didSucceed(retVal)) { editset.rollback(ckp_label); // *sync* // "Server: Error in DBEditObject.finalizeRemove()" // "Custom code disapproved of deleting element from field {0}." return Ganymede.createErrorDialog(this.getGSession(), ts.l("finalizeRemove.myError"), ts.l("finalizeRemove.badDelete", field.getName())); } } } else { // scalar field 0 is the ContainerField for an // embedded object. Note that setting this field to // null will not unlink us from the container object, // since the ContainerField pointer is a generic one // (it has no symmetric relationship information // contained in the invid field definition) retVal = ReturnVal.merge(retVal, field.setValueLocal(null)); // *sync* if (!ReturnVal.didSucceed(retVal)) { editset.rollback(ckp_label); // *sync* // "Server: Error in DBEditObject.finalizeRemove()" // "Custom code disapproved of clearing the value held in field {0}." return Ganymede.createErrorDialog(this.getGSession(), ts.l("finalizeRemove.myError"), ts.l("finalizeRemove.badScalarClear", field.getName())); } } } // Finally we need to take care of any back links. This scans // all objects which have asymmetric invid fields pointing to // us, checks them out for editing if they are not already // checked out by this session, and takes this object's Invid // out of all fields in those objects. retVal = attemptAsymBackLinkClear(true); if (!ReturnVal.didSucceed(retVal)) { editset.rollback(ckp_label); // *sync* return retVal; } editset.popCheckpoint(ckp_label); return retVal; } finally { // make sure we clear deleting before we return deleting = false; } } /** * <p>This method is used to find all objects which point to us * through non-symmetric links, edit them, and break the link. We * do this by consulting the global Ganymede.db.backPointers {@link * arlut.csd.ganymede.server.DBLinkTracker DBLinkTracker} to get the * list of objects which point to us, and doing the unlink in a * fashion similar to InvidDBField.unbindAll().</p> * * <p><b>This method is private, and is not to be called by any code outside * of this class.</b></p> * * @param local if true, this operation will be performed without regard * to permissions limitations. * * @return null on success, or a ReturnVal with an error dialog encoded on failure */ private final ReturnVal attemptAsymBackLinkClear(boolean local) { ReturnVal retVal = null; /* -- */ if (false) { System.err.println("Entering attemptAsymBackLinkClear() for object " + toString()); } Set<Invid> linkSources = Ganymede.db.aSymLinkTracker.getForwardLinkSources(getDBSession(), getInvid()); for (Invid remote: linkSources) { retVal = ReturnVal.merge(retVal, clearBackLink(remote, local)); if (!ReturnVal.didSucceed(retVal)) { return retVal; // finalizeRemove() will rollback } } return retVal; } /** * <p>This method is called by attemptAsymBackLinkClear(), and is * responsible for checking the object with Invid remote out for * editing, and clearing our own Invid out of all of the remote * object's asymmetric Invid fields.</p> * * <p>This method does no checkpointing, so * attemptAsymBackLinkClear() has to do that for us.</p> * * @param remote An Invid for an object that we believe has * asymmetric forward links to us. * @param local If true, we won't do a permissions check before trying to edit the * remote object. * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ private ReturnVal clearBackLink(Invid remote, boolean local) { short targetField; DBEditObject oldRef = null; DBObject remobj; InvidDBField oldRefField = null; DBSession session = getDBSession(); ReturnVal retVal = ReturnVal.success(); ArrayList<Short> fieldsToUnbind = new ArrayList<Short>(); /* -- */ // check to see if we have permission to anonymously unlink // this field from the target field, else go through the // GanymedeSession layer to have our permissions checked. // note that if the GanymedeSession layer has already checked out the // object, session.editDBObject() will return a reference to that // version, and we'll lose our security bypass.. for that reason, // we also use the anon variable to instruct dissolve to disregard // write permissions if we have gotten the anonymous OK remobj = session.viewDBObject(remote); if (remobj == null) { // "DBEditObject.clearBackLink(): Couldn''t find old reference" // "Your operation could not succeed because {0} was linked to a remote reference {1} that could not be resolved for unlinking.\n\nThis is a serious logic error in the server. You should run the invid diagnostics in the admin console." return Ganymede.createErrorDialog(this.getGSession(), ts.l("clearBackLink.badRef"), ts.l("clearBackLink.badRefTxt", toString(), remote.toString())); } if (false) { System.err.println("DBEditObject.clearBackLink(): Clearing link in " + getGSession().describe(remote)); } // loop over the invid fields in the target, get a list of fields we need to unlink. Invid myInvid = getInvid(); // get a thread sync'ed snapshot of the fields in the remote object Vector<DBField> fieldVect = remobj.getFieldVect(); for (DBField tmpField: fieldVect) { if (!(tmpField instanceof InvidDBField)) { continue; } // if the field is symmetric, the InvidDBField logic should // take care of the unbinding, so we won't need to take care // of it ourselves. if (tmpField.getFieldDef().isSymmetric()) { continue; } // If the invid field we're checking out doesn't reference // us, don't bother with it. if (tmpField.isVector()) { if (!tmpField.containsElementLocal(myInvid)) { continue; } } else { Invid tempInvid = (Invid) tmpField.getValueLocal(); if (!myInvid.equals(tempInvid)) { continue; } } // ok, we know we need to do the unbinding for this field. fieldsToUnbind.add(tmpField.getID()); } if (remobj instanceof DBEditObject) { oldRef = (DBEditObject) remobj; } else { if (local || getGSession().getPermManager().getPerm(remobj).isEditable()) { oldRef = (DBEditObject) session.editDBObject(remote); } } if (oldRef == null) { // it's there, but we can't unlink it // "DBEditObject.clearBackLink(): Couldn''t unlink old reference" // "You don''t have permission to unlink object {0} from one or more fields in object {1}." return Ganymede.createErrorDialog(this.getGSession(), ts.l("clearBackLink.badUnlink"), ts.l("clearBackLink.perm", getLabel(), remobj.toString())); } for (Short remote_fieldid: fieldsToUnbind) { targetField = remote_fieldid.shortValue(); retVal.addRescanField(remote, targetField); // are we allowed to ignore permissions on this field? try { oldRefField = oldRef.getInvidField(targetField); } catch (ClassCastException ex) { // "DBEditObject.clearBackLink(): Couldn''t unlink old reference" // "Your operation could not succeed due to an error in the server''s custom schema code. Target field {0} in object {1} is not an Invid field." return Ganymede.createErrorDialog(this.getGSession(), ts.l("clearBackLink.badUnlink"), ts.l("clearBackLink.badTarget", oldRef.getField(targetField).getName(), oldRef.getLabel())); } if (oldRefField == null) { // editDBObject() will create undefined fields for all fields defined // in the DBObjectBase, so if we got a null result we have a schema // corruption problem. // "DBEditObject.clearBackLink(): Couldn''t unlink old reference" // "Your operation could not succeed due to an inconsistency in the server''s database schema. Target field number {0} in object {1} does not exist, or you do not have permission to access this field." return Ganymede.createErrorDialog(this.getGSession(), ts.l("clearBackLink.badUnlink"), ts.l("clearBackLink.badSchema", Short.valueOf(targetField), oldRef.getLabel())); } try { // clear any reference in this field to us, if we can retVal = ReturnVal.merge(retVal, oldRefField.dissolve(getInvid(), local)); if (!ReturnVal.didSucceed(retVal)) { return retVal; } } catch (IllegalArgumentException ex) { System.err.println("hm, couldn't dissolve a reference in " + getLabel()); System.err.println("Did do an anonymous edit on target"); throw (IllegalArgumentException) ex; } } // tell the client that it needs to rescan the old remote ends of this binding return retVal; } /** * <p>This method provides a pre-commit hook that runs after the * user has hit commit but before the system has established write * locks for the commit.</p> * * <p>The intended purpose of this hook is to allow objects that * dynamically maintain hidden label fields to update those fields * from the contents of the object's other fields at commit * time.</p> * * <p>This method runs in a checkpointed context. If this method * fails in any operation, you should return a ReturnVal with a * failure dialog encoded, and the transaction's commit will be * blocked and a dialog explaining the problem will be presented to * the user.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public ReturnVal preCommitHook() { return null; } /** * <p>This method performs verification for the first phase of * the two-phase commit algorithm. If this object returns * true from commitPhase1() when called during an editSet's * commit() routine, this object CAN NOT refuse commit() * at a subsequent point. Once commitPhase1() is called, * the object CAN NOT be changed until the transaction * is either committed, abandoned, or released from the * commit process by the * {@link arlut.csd.ganymede.server.DBEditObject#release(boolean) release()} * method.</p> * * <p>This method may be subclassed by application objects that need * to include extra-Ganymede processes in the two-phase commit * protocol. If a particular subclass of DBEditObject does not need * to involve outside processes in the full two-phase commit * protocol, this method should not be overridden.</p> * * <p>If this method is overridden, be sure and call * setCommitting(true) before doing anything else. Failure to call * setCommitting() in this method will cause the two phase commit * mechanism to behave unpredictably.</p> * * <p><b>WARNING!</b> this method is called at a time when portions * of the database are locked for the transaction's integration into * the database. You must not call methods that seek to gain a lock * on the Ganymede database. At this point, this means no composite * queries on embedded object types, where you seek an object based * on a field in an embedded object and in the object itself, using * the GanymedeSession query calls, or else you will lock the server.</p> * * <p>This method should NEVER try to edit or change any DBEditObject * in the server.. at this point in the game, the server has fixed the * transaction working set and is depending on commitPhase1() not trying * to make changes.</p> * * @see arlut.csd.ganymede.server.DBEditSet * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public synchronized ReturnVal commitPhase1() { setCommitting(true); // if we are deleting or dropping this user, don't bother to check // for consistency switch (this.getStatus()) { case ObjectStatus.DELETING: case ObjectStatus.DROPPING: return null; } // if we have enableOversight turned on, let's check and see if // this object is currently consistent. If it is not, and it was // before this transaction started, report the problem. // If this object is in an inconsistent state, and was before the // transaction started, don't block the commit, because the object // may have been rendered inconsistent by forcible deletion of // other objects previously linked by this object, and we don't // want to block a commit that a non-privileged, non-supergash // admin may not necessarily be able to fix. if (getGSession().enableOversight) { ReturnVal retVal = consistencyCheck(this); if (ReturnVal.didSucceed(retVal)) { return retVal; // no problem } if (original != null) { ReturnVal retVal2 = consistencyCheck(original); if (!ReturnVal.didSucceed(retVal2)) { return null; // we were already inconsistent, so don't complain } } // we were consistent before (or newly created in an // inconsistent state), so complain return retVal; } else { return null; } } /** * This method returns true if this object has already gone * through phase 1 of the commit process, which requires * the DBEditObject not to accept further changes. * * {@link arlut.csd.ganymede.server.DBField DBField}'s * {@link arlut.csd.ganymede.server.DBField#isEditable(boolean) isEditable()} * method consults this method to determine whether to allow * editing of fields. While a DBEditObject is in the committing * process, no changes to fields will be allowed. */ public final boolean isCommitting() { return commitSemaphore.isSet(); } /** * This method is intended to be used by subclasses to set the * state of this object's committing flag. */ protected final void setCommitting(boolean state) { commitSemaphore.set(state); } /** * <p>This method is a hook for subclasses to override to * pass the phase-two commit command to external processes.</p> * * <p>For normal usage this method would not be overridden. For * cases in which change to an object would result in an external * process being initiated whose <b>success or failure would not * affect the successful commit of this DBEditObject in the * Ganymede server</b>, the process invocation should be placed here, * rather than in * {@link arlut.csd.ganymede.server.DBEditObject#commitPhase1() commitPhase1()}.</p> * * <p>commitPhase2() is generally the last method called on a * DBEditObject before it is discarded by the server in the * {@link arlut.csd.ganymede.server.DBEditSet DBEditSet} * {@link arlut.csd.ganymede.server.DBEditSet#commit(java.lang.String) commit()} method.</p> * * <p>Subclasses that override this method may wish to make this method * synchronized.</p> * * <p><b>WARNING!</b> this method is called at a time when portions * of the database are locked for the transaction's integration into * the database. You must not call methods that seek to gain a lock * on the Ganymede database. At this point, this means no composite * queries on embedded object types, where you seek an object based * on a field in an embedded object and in the object itself, using * the GanymedeSession query calls, or else you will lock the server.</p> * * <p>This method should NEVER try to edit or change any DBEditObject * in the server.. at this point in the game, the server has fixed the * transaction working set and is depending on commitPhase2() not trying * to make changes internal to the server.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> */ public void commitPhase2() { return; } /** * <p>A hook for subclasses to use to clean up any external * resources allocated for this object. This method can be called * after commitPhase1() has been called, or it may be called at any * time to indicate that this object is being withdrawn from the * transaction (as by a checkpoint rollback). This method <b>will * not</b> be called after commitPhase2() has been called. If you * do anything external in commitPhase2(), make sure that all * resources allocated for this object (at any time in this object's * editing life-cycle) are released before commitPhase2() * completes.</p> * * <p>Ordinarily, there is no need for customizers to override this * method. The only reason to override the release method is if you * need to do maintenance on external data structures or connections * that were created in commitPhase1() or when this DBEditObject was * created.</p> * * <p>If <finalAbort> is true, the transaction for which this * DBEditObject was created is being completely abandoned (if * isCommitting() returns true), or this object is being dropped out * of the transaction by a checkpoint rollback. In either case, a * customizer may want to clean up all external structures or * connections that were created either at the time this * DBEditObject was created/checked-out and/or that were created by * commitPhase1().</p> * * <p>If <finalAbort> is false, isCommitting() should always * return true. In this case, one of the DBEditObjects in the * transaction returned false from a later commitPhase1() call * and all objects that had their commitPhase1() methods called * previously will be revisted and release(false) will be called * on them. Customizers may want to clean up any external structures * or connections that were established in commitPhase1().</p> * * <p>Remember, you will usually want to perform external actions in * commitPhase2(), in which case release() is not needed. release() * is only useful when you allocate external structures or * connections when the object is created or during commitPhase1().</p> * * <p>It is safe to call release() from your commitPhase2() method * if you wish to have one place to clean up structures allocated by * initializeNewObject() or commitPhase1().</p> * * <p>Customizers subclassing this method may want to keep a couple of * things in mind. First, the default release method is not synchronized, * and it basically just clear a boolean flag (call setCommitting(false)) to * indicate that edit methods on this object may once again go forward. * You may want to synchronize your release method if you do anything * at all fancy. More importantly, it is essential that you call setCommitting(false) * if <finalAbort> is false so that this object can be edited afterwards.</p> * * <p><b>WARNING!</b> this method is called at a time when portions * of the database are locked for the transaction's integration into * the database. You must not call methods that seek to gain a lock * on the Ganymede database. At this point, this means no composite * queries on embedded object types, where you seek an object based * on a field in an embedded object and in the object itself, using * the GanymedeSession query calls, or else you will lock the server.</p> * * <p>To be overridden on necessity in DBEditObject subclasses.</p> * * @param finalAbort If true, this object is being dropped, either due to an * aborted transaction or a checkpoint rollback. */ public void release(boolean finalAbort) { if (!finalAbort) { setCommitting(false); } } // *** // // Checkpoint / Rollback support // // *** /** * <p>Returns a hashtable mapping Short field id's to their current * value, used by the {@link arlut.csd.ganymede.server.DBEditSet DBEditSet} * intra-transaction checkpointing logic to capture this object's * state at a given time.</p> * * <p>Each subclass of {@link arlut.csd.ganymede.server.DBField DBField} * is responsible for implementing its own version of * {@link arlut.csd.ganymede.server.DBField#checkpoint() checkpoint()} to * stuff its state into an Object for inclusion in this method's * hashtable.</p> */ final Hashtable<Short, Object> checkpoint() { Short key; Object value; Hashtable<Short, Object> result = new Hashtable<Short, Object>(); /* -- */ // sync'ing on fieldAry is safe enough to do here since we don't // call any methods within the sync block that should trigger any // external synchronization issues Vector<DBField> fields = this.getFieldVect(); for (DBField field: fields) { key = Short.valueOf(field.getID()); value = field.checkpoint(); if (value != null) { result.put(key, value); } else { // hack, hack.. we're using a reference // to this object to represent a null value result.put(key, this); } } return result; } /** * <p>Reinstates this object's state from a hashtable returned * by {@link arlut.csd.ganymede.server.DBEditObject#checkpoint() checkpoint()}, * used by the {@link arlut.csd.ganymede.server.DBEditSet DBEditSet} * intra-transaction checkpoint rollback logic to restore this object's * state at a given time.</p> * * <p>Each subclass of {@link arlut.csd.ganymede.server.DBField DBField} * is responsible for implementing its own version of * {@link arlut.csd.ganymede.server.DBField#rollback(java.lang.Object) rollback()} to * restore its state.</p> */ synchronized final void rollback(Map<Short, Object> ckpoint) { for (Map.Entry<Short, Object> entry: ckpoint.entrySet()) { DBField field = getField(entry.getKey().shortValue()); Object value = entry.getValue(); // again, we use a reference to ourselves as a // hackish way of representing null in the // hashtable if (value == this) { field.rollback(null); } else { field.rollback(value); } } } /** * <p>This method is used to write out this DBEditObject and the * deltas between this DBEditObject and the original checked in * version of this DBEditObject to the provided XMLDumpContext. The * constraints specified in the {@link * arlut.csd.ganymede.server.SyncRunner} registered with the * XMLDumpContext parameter will be used to control what fields are * written.</p> * * <p>This method should only be called on a DBEditObject that is known * to have been edited.. calling this method on a newly created * object should not be done.. use the normal DBObject emitXML() * method in an <object_delta> element's <after> element * instead.</p> */ public final void emitXMLDelta(XMLDumpContext xmlOut) throws IOException { boolean fieldChanged; /* -- */ if (getStatus() != ObjectStatus.EDITING) { // "Can''t call emitXMLDelta on a DBEditObject in the CREATING, DELETING, or DROPPING state." throw new IllegalArgumentException(ts.l("emitXMLDelta.bad_state")); } xmlOut.startElementIndent("object_delta"); xmlOut.indentOut(); xmlOut.setBeforeStateDumping(true); xmlOut.startElementIndent("before"); xmlOut.indentOut(); xmlOut.startElementIndent("object"); xmlOut.attribute("type", XMLUtils.XMLEncode(getTypeName())); xmlOut.attribute("id", original.getLabel()); xmlOut.attribute("oid", getInvid().toString()); xmlOut.indentOut(); for (DBObjectBaseField fieldDef: objectBase.getFieldsInDisplayOrder()) { DBField myField = this.getField(fieldDef.getID()); DBField origField = original.getField(fieldDef.getID()); if (origField == null) { // not present in the before state, don't write anything for it continue; } else if (myField != null) { // we had this field in the before state, let's see if we // need to write it out, either because this field was // deleted from the after state and our sync constraints // require us to write it out, or because the field is // present both before and after, but it has changed and // our sync constraints require us to write it out in that // circumstance. if ((!myField.isDefined() && xmlOut.mayInclude(origField))|| (myField.isDefined() && xmlOut.shouldInclude(myField, origField))) { origField.emitXML(xmlOut); } } } xmlOut.indentIn(); xmlOut.endElementIndent("object"); xmlOut.indentIn(); xmlOut.endElementIndent("before"); xmlOut.setBeforeStateDumping(false); xmlOut.startElementIndent("after"); xmlOut.indentOut(); xmlOut.startElementIndent("object"); xmlOut.attribute("type", XMLUtils.XMLEncode(getTypeName())); xmlOut.attribute("id", getLabel()); xmlOut.attribute("oid", getInvid().toString()); xmlOut.indentOut(); for (DBObjectBaseField fieldDef: objectBase.getFieldsInDisplayOrder()) { DBField myField = this.getField(fieldDef.getID()); DBField origField = original.getField(fieldDef.getID()); if (myField == null || !myField.isDefined()) { // not present in the after state, don't write anything for it continue; } else { // we have this field in the after state, let's see if we // need to write it out, either because this field was // newly created in the after state and our sync // constraints require us to write it out, or because the // field is present both before and after, but it has // changed and our sync constraints require us to write it // out in that circumstance. if ((origField == null && xmlOut.mayInclude(myField))|| (origField != null && xmlOut.shouldInclude(myField, origField))) { myField.emitXML(xmlOut); } } } xmlOut.indentIn(); xmlOut.endElementIndent("object"); xmlOut.indentIn(); xmlOut.endElementIndent("after"); xmlOut.indentIn(); xmlOut.endElementIndent("object_delta"); } /** * <p>This method is used to generate a String describing the difference * between the current state of the DBEditObject and the original * object's state.</p> * * <p>This method can also be used if this object is newly created.. in * this case, it will just return a string containing many 'FieldAdded' * entries.</p> * * @return null if no difference was found */ public synchronized String diff() { return this.diff(null); } /** * <p>This method does two things. First and foremost, it is used * to generate a String describing the difference between the * current state of the DBEditObject and the original object's * state.</p> * * <p>This method can also be used if this object is newly created.. in * this case, it will just return a string containing many 'FieldAdded' * entries.</p> * * <p>The second purpose of this method is to generate entries in * the changedFieldDefs Set, listing those DBObjectBaseFields for * which we have observed a value change when comparing this * object's state with its original. We do this as part of the diff * algorithm because the original DBField definition only provided * one method to compare two fields for differences, and that is the * getDiffString() method. Since we're calling that here anyway, * recording the definition of fields that we know changed is an * extremely cheap win. The changedFieldDefs Set is used in the * DBEditSet class to update time stamps in the DBObjectBaseFields, * so that builder tasks can tell whether they have been run since * any of a given field have been changed in a given * DBObjectBase.</p> * * @param changedFieldDefs If not null, this parameter will be a Set * that the diff algorithm should insert DBObjectBaseFields whose * value was found to have changed in this diff. * * @return null if no difference was found */ public final synchronized String diff(Set<DBObjectBaseField> changedFieldDefs) { boolean diffFound = false; DBField origField, currentField; StringBuilder added = null; StringBuilder deleted = null; StringBuilder changed = null; /* -- */ // algorithm: iterate over base.getFieldsInFieldOrder() to find all fields // possibly contained in the object.. for each field, check to // see if the value has changed. if so, emit a before and after // diff. if one has a field and the other does not, indicate // the change. // // in the case of vectors, the change description can be a simple // delta (x added, y removed) // note that we're counting on objectBase.sortedFields not being // changed while we're iterating here.. this is an ok assumption, // since only the loader and the schema editor will trigger changes // in sortedFields. if (debug) { System.err.println("Entering diff for object " + getLabel()); } for (DBObjectBaseField fieldDef: objectBase.getFieldsInFieldOrder()) { // we don't care if certain fields change, as they are // guaranteed to change whenever a transaction commits if (fieldDef.getID() == SchemaConstants.CreationDateField || fieldDef.getID() == SchemaConstants.CreatorField || fieldDef.getID() == SchemaConstants.ModificationDateField || fieldDef.getID() == SchemaConstants.ModifierField) { continue; } if (debug) { System.err.println("Comparing field " + fieldDef.getName()); } // if we're newly created, we'll just treat the old field as // non-existent. if (original == null) { origField = null; if (changedFieldDefs != null) { changedFieldDefs.add(fieldDef); } } else { origField = original.getField(fieldDef.getID()); } currentField = this.getField(fieldDef.getID()); if ((origField == null || !origField.isDefined()) && (currentField == null || !currentField.isDefined())) { continue; } if (((origField == null) || !origField.isDefined()) && ((currentField != null) && currentField.isDefined())) { if (changedFieldDefs != null) { changedFieldDefs.add(fieldDef); } if (added == null) { added = new StringBuilder(); } if (okToLogField(currentField)) { // "\t{0}: {1}\n" added.append(ts.l("diff.field_template", fieldDef.getName(), currentField.getValueString())); } else { // "\t{0}\n" added.append(ts.l("diff.anon_field_template", fieldDef.getName())); } diffFound = true; if (debug) { System.err.println("Field added: " + fieldDef.getName() + "\nValue: " + currentField.getValueString() + "\n"); } } else if (((currentField == null) || !currentField.isDefined()) && ((origField != null) && origField.isDefined())) { if (changedFieldDefs != null) { changedFieldDefs.add(fieldDef); } if (deleted == null) { deleted = new StringBuilder(); } if (okToLogField(origField)) { // "\t{0}: {1}\n" deleted.append(ts.l("diff.field_template", fieldDef.getName(), origField.getValueString())); } else { // "\t{0}\n" deleted.append(ts.l("diff.anon_field_template", fieldDef.getName())); } diffFound = true; if (debug) { System.err.println("Field deleted: " + fieldDef.getName() + "\nValue: " + origField.getValueString() + "\n"); } } else { String diff = currentField.getDiffString(origField); if (diff != null) { if (changedFieldDefs != null) { changedFieldDefs.add(fieldDef); } if (changed == null) { changed = new StringBuilder(); } if (okToLogField(currentField) && okToLogField(origField)) { changed.append(fieldDef.getName()); changed.append("\n"); changed.append(diff); if (debug) { System.err.println("Field changed: " + fieldDef.getName() + "\n" + diff); } } else { changed.append(fieldDef.getName()); changed.append("\n"); } diffFound = true; } } } if (diffFound) { StringBuilder result = new StringBuilder(); if (added != null && added.length() > 0) { // "Fields Added:\n\n{0}\n" result.append(ts.l("diff.added", added)); } if (changed != null && changed.length() > 0) { // "Fields Changed:\n\n{0}\n" result.append(ts.l("diff.changed", changed)); } if (deleted != null && deleted.length() > 0) { // "Fields Deleted:\n\n{0}\n" result.append(ts.l("diff.deleted", deleted)); } return result.toString(); } else { return null; } } /*---------------------------------------------------------- Convenience methods for our customization subclasses ----------------------------------------------------------*/ /** * Convenience method for our customization subclasses, returns * a reference to the server's internal 'supergash' session * if a DBEditObject subclass needs to do queries, etc., on * the server internally. */ protected final GanymedeSession internalSession() { return Ganymede.getInternalSession(); } }