/* GASH 2 InvidDBField.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.DataInput; import java.io.DataOutput; import java.io.IOException; import java.rmi.RemoteException; import java.util.Vector; import arlut.csd.JDialog.JDialogBuff; import arlut.csd.Util.RandomUtils; import arlut.csd.Util.TranslationService; import arlut.csd.Util.VectorUtils; 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.QueryResult; import arlut.csd.ganymede.common.ReturnVal; import arlut.csd.ganymede.common.SchemaConstants; import arlut.csd.ganymede.rmi.db_field; import arlut.csd.ganymede.rmi.invid_field; /*------------------------------------------------------------------------------ class InvidDBField ------------------------------------------------------------------------------*/ /** * <p>InvidDBField is a subclass of {@link arlut.csd.ganymede.server.DBField DBField} * for the storage and handling of {@link arlut.csd.ganymede.common.Invid Invid} * fields in the {@link arlut.csd.ganymede.server.DBStore DBStore} on the Ganymede * server.</p> * * <p>The Ganymede client talks to InvidDBFields through the * {@link arlut.csd.ganymede.rmi.invid_field invid_field} RMI interface.</p> * * <p>This class implements one of the most fundamental pieces of logic in the * Ganymede server, the object pointer/object binding logic. Whenever the * client calls setValue(), setElement(), addElement(), or deleteElement() * on an InvidDBField, the object being pointed to by the Invid being set * or cleared will be checked out for editing and the corresponding back * pointer will be set or cleared as appropriate.</p> * * <p>In other words, the InvidDBField logic guarantees that all * objects references in the server are symmetric. If one object * points to another via an InvidDBField, the target of that pointer * will point back, either through a field explicitly specified in the * schema, or through the {@link * arlut.csd.ganymede.server.DBLinkTracker DBLinkTracker} referenced * in the DBStore {@link * arlut.csd.ganymede.server.DBStore#aSymLinkTracker aSymLinkTracker} * variable.</p> * * @author Jonathan Abbey, jonabbey@arlut.utexas.edu, ARL:UT */ public final class InvidDBField extends DBField implements invid_field { static final boolean debug = false; /** * TranslationService object for handling string localization in * the Ganymede server. */ static final TranslationService ts = TranslationService.getTranslationService("arlut.csd.ganymede.server.InvidDBField"); // --- /** * <p>We'll cache the choiceList from our parent in case we're doing * a large vector add/delete. Any time we change our value/values * actually contained in this field, we'll null this out.</p> * * <p>Note that having this here costs us 4 bytes RAM for every * InvidDBField held in the Ganymede server's database, but without * it we'll have an extraordinarily painful time doing mass * adds/deletes.</p> * * <p>Consulted by {#verifyNewValue(java.lang.Object, boolean)}. */ private QueryResult qr = null; /** * Receive constructor. Used to create a InvidDBField from a * {@link arlut.csd.ganymede.server.DBStore DBStore}/{@link arlut.csd.ganymede.server.DBJournal DBJournal} * DataInput stream. */ InvidDBField(DBObject owner, DataInput in, DBObjectBaseField definition) throws IOException { super(owner, definition.getID()); this.value = null; receive(in, definition); } /** * <p>No-value constructor. Allows the construction of a * 'non-initialized' field, for use where the * {@link arlut.csd.ganymede.server.DBObjectBase DBObjectBase} * definition indicates that a given field may be present, * but for which no value has been stored in the * {@link arlut.csd.ganymede.server.DBStore DBStore}.</p> * * <p>Used to provide the client a template for 'creating' this * field if so desired.</p> */ InvidDBField(DBObject owner, DBObjectBaseField definition) { super(owner, definition.getID()); if (isVector()) { this.value = new Vector(); } else { this.value = null; } } /** * Copy constructor. */ public InvidDBField(DBObject owner, InvidDBField field) { super(owner, field.getID()); if (isVector()) { this.value = new Vector(field.getVectVal()); } else { this.value = field.value; } } /** * Scalar value constructor. */ public InvidDBField(DBObject owner, Invid value, DBObjectBaseField definition) { super(owner, definition.getID()); if (definition.isArray()) { // "scalar value constructor called on vector field {0} in object {1}" throw new IllegalArgumentException(ts.l("init.type_mismatch", getName(), this.owner.getLabel())); } this.value = value; } /** * Vector value constructor. */ public InvidDBField(DBObject owner, Vector values, DBObjectBaseField definition) { super(owner, definition.getID()); if (!definition.isArray()) { // "vector value constructor called on scalar field {0} in object {1}" throw new IllegalArgumentException(ts.l("init.type_mismatch2", getName(), this.owner.getLabel())); } if (values == null) { this.value = new Vector(); } else { this.value = new Vector(values); } } public Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } /** * This method is used to write the contents of this field to the * Ganymede.db file and/or to the Journal file. */ @Override void emit(DataOutput out) throws IOException { if (isVector()) { Vector<Invid> values = (Vector<Invid>) getVectVal(); out.writeInt(values.size()); for (Invid invid: values) { out.writeShort(invid.getType()); out.writeInt(invid.getNum()); } } else { Invid invid = (Invid) this.value; try { out.writeShort(invid.getType()); out.writeInt(invid.getNum()); } catch (NullPointerException ex) { System.err.println(this.owner.getLabel() + ":" + getName() + ": void value in emit"); if (invid == null) { System.err.println(this.owner.getLabel() + ":" + getName() + ": field value itself is null"); } throw ex; } } } /** * This method is used to read the contents of this field from the * Ganymede.db file and/or from the Journal file. */ @Override void receive(DataInput in, DBObjectBaseField definition) throws IOException { int count; /* -- */ if (definition.isArray()) { if (Ganymede.db.isLessThan(2,3)) { count = in.readShort(); } else { count = in.readInt(); } if (count > 0) { this.value = new Vector(count); // a cast of convenience.. Vector v = (Vector) this.value; for (int i = 0; i < count; i++) { v.add(Invid.createInvid(in.readShort(), in.readInt())); } } else { this.value = new Vector(); } } else { this.value = Invid.createInvid(in.readShort(), in.readInt()); } this.qr = null; } /** * This method is used when the database is being dumped, to write * out this field to disk. */ @Override synchronized void emitXML(XMLDumpContext xmlOut) throws IOException { // if we're the containing object field in an embedded object, we // don't need to describe ourselves to the XML file.. our // ownership will be implicitly recorded in the structure of the // XML file if (this.owner.isEmbedded() && getID() == 0 && !xmlOut.isDeltaSyncing()) { return; } xmlOut.startElementIndent(this.getXMLName()); if (!isVector()) { emitInvidXML(xmlOut, value(), isEditInPlace()); } else { Vector<Invid> values = (Vector<Invid>) getVectVal(); for (Invid invid: values) { if (!isEditInPlace()) { xmlOut.indentOut(); xmlOut.indent(); xmlOut.indentIn(); } emitInvidXML(xmlOut, invid, isEditInPlace()); } xmlOut.indent(); } xmlOut.endElement(this.getXMLName()); } /** * <p>This method writes out an Invid in XML form to a Ganymede * XML data dump stream.</p> * * <p>Whenever Ganymede writes out an Invid to an XML data dump, it * uses an <invid> element with a minimum two attributes, type * and id. type is the name of the object type that the invid * points to, and id is the uniquely identifying label for the * target object.</p> * * <p>The XMLDumpContext passed into emitInvidXML() can also cause * emitInvidXML() to write out an oid attribute containing the * target object's raw Invid in "typenum:objnum" format, along with * any extra attributes provided by our owner type's * getForeignSyncKeys() method.</p> * * <p>All this is a bit different if this InvidDBField is an * edit-in-place field. In that case, emitInvidXML will simply * write out the embedded object, in place of an invid element.</p> */ public void emitInvidXML(XMLDumpContext xmlOut, Invid invid, boolean asEmbedded) throws IOException { DBObject target = null; DBField targetField; /* -- */ // we provide a DBSession from the XMLDumpContext so that we can // emit the 'after' label of the target invid if the target was // modified in the session, even if our owner was never checked // out for editing or viewing in the session. target = this.owner.lookupInvid(invid, xmlOut.isBeforeStateDumping(), xmlOut.getDBSession()); if (target == null) { throw new IllegalArgumentException(ts.l("emitInvidXML.bad_invid", this.toString(), invid)); } if (asEmbedded) { xmlOut.indentOut(); target.emitXML(xmlOut); xmlOut.indentIn(); } else { xmlOut.startElement("invid"); xmlOut.attribute("type", XMLUtils.XMLEncode(target.getTypeName())); xmlOut.attribute("id", target.getLabel()); // getLabel() gives us the XML-friendly label if (xmlOut.isSyncing()) { xmlOut.attribute("oid", invid.toString()); // // first get any extra Invid element attributes from the // object that we are writing out // DBEditObject hook = this.owner.getHook(); String extras[] = hook.getForeignSyncKeys(invid, this.owner, target, xmlOut.getSyncChannelName(), xmlOut.isBeforeStateDumping()); if (extras != null && extras.length > 0) { if (extras.length % 2 != 0) { // "InvidDBField.emitInvidXML(): mismatched attribute/value pairs returned from getForeignSyncKeys() call on {0}" throw new RuntimeException(ts.l("emitInvidXML.bad_foreign_keys", this.owner.toString())); } for (int i = 0; i < extras.length; i = i + 2) { String name = extras[i]; String value = extras[i+1]; if (name.equals("id") || name.equals("num") || name.equals("type") || name.equals("oid")) { // "InvidDBField.emitInvidXML(): improper use of a reserved attribute name in attribute/value pairs returned from getForeignSyncKeys call on {0}" throw new RuntimeException(ts.l("emitInvidXML.bad_attribute_name", this.owner.toString())); } xmlOut.attribute(name, value); } } // // now get any extra Invid element attributes from the // object that we are targeting // hook = target.getHook(); extras = hook.getMyExtraInvidAttributes(target, xmlOut.getSyncChannelName(), xmlOut.isBeforeStateDumping()); if (extras != null && extras.length > 0) { if (extras.length % 2 != 0) { // "InvidDBField.emitInvidXML(): mismatched attribute/value pairs returned from getForeignSyncKeys() call on {0}" throw new RuntimeException(ts.l("emitInvidXML.bad_foreign_keys", this.owner.toString())); } for (int i = 0; i < extras.length; i = i + 2) { String name = extras[i]; String value = extras[i+1]; if (name.equals("id") || name.equals("num") || name.equals("type") || name.equals("oid")) { // "InvidDBField.emitInvidXML(): improper use of a reserved attribute name in attribute/value pairs returned from getForeignSyncKeys call on {0}" throw new RuntimeException(ts.l("emitInvidXML.bad_attribute_name", this.owner.toString())); } xmlOut.attribute(name, value); } } } xmlOut.endElement("invid"); } } /** * This method is intended to be called when this field is being checked into * the database. Subclasses of DBField will override this method to clean up * data that is cached for speed during editing. */ public void cleanup() { qr = null; super.cleanup(); } // **** // // type-specific accessor methods // // **** public Invid value() { if (isVector()) { // "scalar accessor called on vector field {0} in object {1}" throw new IllegalArgumentException(ts.l("value.type_mismatch", getName(), this.owner.getLabel())); } return (Invid) value; } public Invid value(int index) { if (!isVector()) { // "vector accessor called on scalar field {0} in object {1}" throw new IllegalArgumentException(ts.l("value.type_mismatch2", getName(), this.owner.getLabel())); } return (Invid) getVectVal().get(index); } /** * <p>This method returns a text encoded value for this InvidDBField * without checking permissions.</p> * * <p>This method avoids checking permissions because it is used on * the server side only and because it is involved in the {@link * arlut.csd.ganymede.server.DBObject#getLabel() getLabel()} logic * for {@link arlut.csd.ganymede.server.DBObject DBObject}</p> * * <p>If this method checked permissions and the getPerm() method * failed for some reason and tried to report the failure using * object.getLabel(), as it does at present, the server could get * into an infinite loop.</p> */ @Override public synchronized String getValueString() { GanymedeSession gsession = null; /* -- */ // where will we go to look up the label for our target(s)? gsession = getGSession(); if (gsession == null) { gsession = Ganymede.internalSession; } // now do the work if (value == null) { return ""; } if (!isVector()) { Invid localInvid = (Invid) this.value(); // XXX note: we don't use our owner's lookupLabel() method // for scalar invid values.. return getRemoteLabel(gsession, localInvid, false); } else { int size = size(); if (size == 0) { return ""; } String entries[] = new String[size]; Invid tmp; for (int i = 0; i < size; i++) { tmp = this.value(i); entries[i] = getRemoteLabel(gsession, tmp, false); } java.util.Arrays.sort(entries, null); StringBuilder result = new StringBuilder(); for (int i = 0; i < entries.length; i++) { if (i > 0) { result.append(","); } result.append(entries[i]); } return result.toString(); } } /** * <p>This method returns the label of an object referenced by an * invid held in this field. If the remote object referenced by the * invid argument is currently being deleted, we'll try to get the * label from the state of that object as it existed at the start of * the current transaction. This is to allow us to do proper * logging of the values deleted from this field in the case of the * string generated by {@link arlut.csd.ganymede.server.DBEditObject#diff() * DBEditObject.diff()} during transaction logging.</p> * * <p>If forceOriginal is set to true, getRemoteLabel will always * try to retrieve the remote object's original label, even if the * remote object has not been deleted by the active transaction.</p> */ private String getRemoteLabel(GanymedeSession gsession, Invid invid, boolean forceOriginal) { if (gsession != null) { /* * okay.. if we are finding the name of the referenced field in * the DBEditSet logging context, our reference might have already * had its fields cleared out.. we want to be able to get access * to the label it had for the purpose of logging this transaction.. * * the DBEditSet commit() logic uses DBEditObject.diff(), which * will call us to get the original value of a invid field (perhaps * before the current version of this field was cleared.. we need * to also be able to present the name of the remote object before * it was cleared.. */ DBObject objectRef = gsession.getDBSession().viewDBObject(invid); if (objectRef != null && (objectRef instanceof DBEditObject)) { DBEditObject eObjectRef = (DBEditObject) objectRef; if (forceOriginal || eObjectRef.getStatus() == ObjectStatus.DELETING) { objectRef = eObjectRef.getOriginal(); } } if (objectRef == null) { // "*** no target for invid {0} ***" return ts.l("getRemoteLabel.nonesuch", invid.toString()); } if (objectRef.isEmbedded()) { return objectRef.getEmbeddedObjectDisplayLabel(); } else { return objectRef.getLabel(); } } else { return invid.toString(); } } /** * OK, this is a bit vague.. getEncodingString() is used by the * new dump system to allow all fields to be properly sorted in the * client's query result table.. a real reversible encoding of an * invid field would *not* be the getValueString() results, but * getValueString() is what we want in the dump result table, so * we'll do that here for now. */ @Override public String getEncodingString() { return getValueString(); } /** * <p>Returns a String representing the change in value between this * field and orig. This String is intended for logging and email, * not for any sort of programmatic activity. The format of the * generated string is not defined, but is intended to be suitable * for inclusion in a log entry and in an email message.</p> * * <p>If there is no change in the field, null will be returned.</p> */ @Override public synchronized String getDiffString(DBField orig) { InvidDBField origI; GanymedeSession gsession = null; /* -- */ if (!(orig instanceof InvidDBField)) { // "Bad field comparison {0}" throw new IllegalArgumentException(ts.l("getDiffString.badtype", getName())); } if (orig == this) { return null; } origI = (InvidDBField) orig; gsession = getGSession(); if (gsession == null) { gsession = Ganymede.internalSession; } if (isVector()) { Vector<Invid> values = (Vector<Invid>) getVectVal(); Vector<Invid> origValues = (Vector<Invid>) origI.getVectVal(); Vector<Invid> deleted = VectorUtils.difference(origValues, values); Vector<Invid> added = VectorUtils.difference(values, origValues); // were there any changes at all? if (deleted.size() == 0 && added.size() == 0) { return null; } else { StringBuilder result = new StringBuilder(); if (deleted.size() != 0) { StringBuilder deleteString = new StringBuilder(); for (int i = 0; i < deleted.size(); i++) { Invid invid = deleted.get(i); if (i > 0) { deleteString.append(", "); } deleteString.append(getRemoteLabel(gsession, invid, true)); // get original if edited } // "\tDeleted: {0}\n" result.append(ts.l("getDiffString.deleted_items", deleteString.toString())); } if (added.size() != 0) { StringBuilder addString = new StringBuilder(); for (int i = 0; i < added.size(); i++) { Invid invid = added.get(i); if (i > 0) { addString.append(", "); } addString.append(getRemoteLabel(gsession, invid, false)); } // "\tAdded: {0}\n" result.append(ts.l("getDiffString.added_items", addString.toString())); } return result.toString(); } } else { if (origI.value.equals(this.value)) { return null; } else { // "\tOld: {0}\n\tNew:{1}\n" return ts.l("getDiffString.scalar", getRemoteLabel(gsession, origI.value(), true), getRemoteLabel(gsession, this.value(), false)); } } } // **** // // methods for maintaining invid symmetry // // **** /** * <p>This private helper method attempts to verify that a * prospective bind operation in an vector add context can succeed * without forcing an unbinding on a scalar remote field.</p> * * <p>This method <b>only</b> checks to see if we're trying to bind * to an already bound scalar InvidDBField. If there are any other * schema problems that would cause a bind to fail, this method will * return a null (success) ReturnVal, trusting the later bind attempt * to fail and produce an informative message.</p> * * @return null on 'no problems' or 'a problem that bind will * detect', and a non-null ReturnVal with a dialog encoded if there * is a scalar conflict in place. */ private final ReturnVal checkBindConflict(Invid newRemote) { short targetField; DBObject remobj; InvidDBField remoteField; DBEditObject myParent; DBSession mySession; /* -- */ if (!getFieldDef().isSymmetric()) { return null; } myParent = (DBEditObject) this.owner; mySession = myParent.getDBSession(); targetField = getFieldDef().getTargetField(); remobj = mySession.viewDBObject(newRemote); if (remobj == null) { return null; // failure that bind will catch } try { remoteField = remobj.getInvidField(targetField); } catch (ClassCastException ex) { return null; } if (remoteField == null) { // the target is either non-edited and doesn't contain the // field in question, or it's been checked out for editing and // the field in question just isn't defined. it's either not // a problem because there's no conflicting bind, or the // schema is screwed up. Not for us to worry about either way. return null; } if (!remoteField.isVector() && remoteField.isDefined()) { Invid myInvid = (Invid) remoteField.getValueLocal(); if (myInvid.equals(myParent.getInvid())) { return null; // rebinding self.. bind() will catch this } // this is the case we care about // "Link Error" // "Your operation could not be performed. The target object {0} can only be linked to one {1} at a time." return Ganymede.createErrorDialog(this.getGSession(), ts.l("checkBindConflict.subj"), ts.l("checkBindConflict.overlink", remobj.getLabel(), this.owner.getTypeName())); } return null; } /** * <p>This method is used to link the remote invid to this checked-out invid * in accordance with this field's defined symmetry constraints.</p> * * <p>This method deals with the back pointers (symmetric or * asymmetric), the forward link is established in InvidDBField * through direct manipulation of the field's value.</p> * * <p>This method will extract the objects referenced by the old and new * remote parameters, and will cause the appropriate invid dbfields in * them to be updated to reflect the change in link status. If either * operation can not be completed, bind will return the system to its * pre-bind status and return false. One or both of the specified * remote objects may remain checked out in the current editset until * the transaction is committed or released.</p> * * <p>It is an error for newRemote to be null; if you wish to undo an * existing binding, use the unbind() method call. oldRemote may * be null if this currently holds no value, or if this is a vector * field and newRemote is being added.</p> * * <p>This method should only be called from synchronized methods within * InvidDBField.</p> * * <p><b>This method is private, and is not to be called by any code outside * of this class.</b></p> * * @param oldRemote the old invid to be replaced * @param newRemote the new invid to be linked * @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 * * @see arlut.csd.ganymede.server.InvidDBField#unbind(arlut.csd.ganymede.common.Invid, boolean) */ private final ReturnVal bind(Invid oldRemote, Invid newRemote, boolean local) { short targetField; DBEditObject eObj = null, oldRef = null, newRef = null; InvidDBField oldRefField = null, newRefField = null; DBSession session = null; boolean anonymous = false, anonymous2 = false; ReturnVal retVal = null; DBObject remobj = null; /* -- */ if (newRemote == null) { // "Null newRemote {0} in object {1}" throw new IllegalArgumentException(ts.l("bind.noremote", getName(), this.owner.getLabel())); } if (!isEditable(local)) { // "Not an editable invid field: {0} in object {1}" throw new IllegalArgumentException(ts.l("bind.noteditable", getName(), this.owner.getLabel())); } if (debug) { System.err.println("InvidDBField.bind(" + oldRemote + ", " + newRemote + ", " + local + ")"); } eObj = (DBEditObject) this.owner; session = eObj.getDBSession(); if (newRemote.equals(oldRemote)) { return null; // success } if (!getFieldDef().isSymmetric()) { if (oldRemote != null) { unlinkTarget(oldRemote); } Ganymede.db.aSymLinkTracker.linkObject(getDBSession(), newRemote, this.owner.getInvid()); } // If we're the container field in an embedded object, we're // done.. we don't want to do a deleteLockObject() on our parent // object because the existence of embedded objects should not // block the deletion of the parent. if (this.owner.objectBase.isEmbedded() && getID() == SchemaConstants.ContainerField) { return null; } if (!getFieldDef().isSymmetric()) { // We need to make sure that we're not trying to link to an // object that is in the middle of being deleted, so we check // that here. If deleteLockObject() returns true, the system // will prevent the target object from being deleted until our // transaction has cleared. // We don't have to worry about oldRemote (if it is indeed not // null), as the DBEditSet marked it as non-deletable when we // added the owner of this field to the transaction. This is // done in DBEditSet.addObject(). We don't want to clear the // delete lock our transaction has on it, because we have to // be able to revert the link to oldRemote if the transaction // is cancelled. if (!DBDeletionManager.deleteLockObject(session.viewDBObject(newRemote), session)) { // "Bind link error" // "Can't forge an asymmetric link between {0} and invid {1}, the target object is being deleted." return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.deletedremote_sub"), ts.l("bind.deletedremote_text", this.getName(), newRemote.toString())); } return null; } // we're symmetric, so find out what field in remote we need to // update targetField = getFieldDef().getTargetField(); // check out the old object and the new object // remove the reference from the old object // add the reference to the new object if (oldRemote != null) { // 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 anonymous variable to instruct dissolve to disregard // write permissions if we have gotten the anonymous OK remobj = session.viewDBObject(oldRemote); if (remobj == null) { // "InvidDBField.bind(): Couldn''t find old reference" // "Your operation could not succeed because field {0} was linked to a remote reference {1} that could not be found \ // for unlinking.\n\nThis is a serious logic error in the server." return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_oldref"), ts.l("bind.no_oldref_text", getName(), oldRemote.toString())); } // see if we are allowed to unlink the remote object without // having permission to edit it generally. anonymous = session.getObjectHook(oldRemote).anonymousUnlinkOK(remobj, targetField, this.owner, this.getID(), getGSession()); // if we're already editing it, just go with that. if (remobj instanceof DBEditObject) { oldRef = (DBEditObject) remobj; } else { if (anonymous || getGSession().getPermManager().getPerm(remobj).isEditable()) { oldRef = session.editDBObject(oldRemote); } else { // "InvidDBField.bind(): Couldn''t unlink from old reference" // "Your operation could not succeed because you don''t have permission to dissolve the link to the old object \ // held in field {0} in object {1}" return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_unlink_sub"), ts.l("bind.no_perms_old", getName(), this.owner.getLabel())); } } // if we couldn't check out the old object for editing, we need to see why. if (oldRef == null) { // this check is not truly thread safe, as the // shadowObject might be cleared by another thread while // we're working.. this isn't a grave risk, but we'll // wrap all of this in a NullPointerException catch just // in case. this check is just for informative purposes, // so we don't mind throwing a null pointer exception in // here, it's not worth doing all of the careful sync work // to lock down this stuff without risk of deadlock try { String edit_username, edit_hostname; DBEditObject editing = remobj.getShadow(); if (editing != null) { if (editing.gSession != null) { edit_username = editing.gSession.getPermManager().getBaseIdentity(); edit_hostname = editing.gSession.getClientHostName(); // "InvidDBField.bind(): Couldn''t unlink from old reference" // "Field {0} could not be unlinked from the {1} {2} object, which is busy being edited by {3} on system {4}" return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_unlink_sub"), ts.l("bind.busy_old", this.getName(), remobj.getLabel(), remobj.getTypeName(), edit_username, edit_hostname)); } // "InvidDBField.bind(): Couldn''t unlink from old reference" // "Field {0} could not be unlinked from the {1} {2} object, which is busy being edited by another user." return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_unlink_sub"), ts.l("bind.busy_old2", this.getName(), remobj.getLabel(), remobj.getTypeName())); } else { // "InvidDBField.bind(): Couldn''t unlink from old reference" // "Field {0} could not be unlinked from the {1} {2} object. // This is probably a temporary condition due to other user activity on the Ganymede server." return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_unlink_sub"), ts.l("bind.busy_old_temp", this.getName(), remobj.getLabel(), remobj.getTypeName())); } } catch (NullPointerException ex) { // "InvidDBField.bind(): Couldn''t unlink from old reference" // "Field {0} could not be unlinked from the {1} {2} object. // This is probably a temporary condition due to other user activity on the Ganymede server." return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_unlink_sub"), ts.l("bind.busy_old_temp", this.getName(), remobj.getLabel(), remobj.getTypeName())); } } try { oldRefField = oldRef.getInvidField(targetField); } catch (ClassCastException ex) { // "InvidDBField.bind(): Couldn''t unlink from old reference" // "Your operation could not succeed due to an error in the server''s schema. Target field {0} in object {1} is not an invid field." return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_unlink_sub"), ts.l("bind.schema_error", oldRef.getField(targetField).getName(), oldRef.getLabel())); } if (oldRefField == null) { // editDBObject() will create undefined fields for all // fields defined in the DBObjectBase as long as the user // had permission to create those fields, so if we got a // null result we either have a schema corruption problem // or a permission to create field problem DBObjectBaseField fieldDef = oldRef.getFieldDef(targetField); if (fieldDef == null) { // "InvidDBField.bind(): Couldn''t unlink from old reference" // "Your operation could not succeed due to a possible inconsistency in the server database. Target field number {0} in object {1} does not exist." return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_unlink_sub"), ts.l("bind.inconsistency", Integer.toString(targetField), oldRef.getLabel())); } else { // "InvidDBField.bind(): Couldn''t unlink from old reference" // "Your operation could not succeed due to a possible inconsistency in the server database. Target field {0} is undefined in object {1}." return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_unlink_sub"), ts.l("bind.inconsistency2", fieldDef.getName(), oldRef.getLabel())); } } } // check to see if we have permission to anonymously link // this field to 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 anonymous2 variable to instruct establish to disregard // write permissions if we have gotten the anonymous OK remobj = session.viewDBObject(newRemote); if (remobj == null) { // "InvidDBField.bind(): Couldn''t find new reference" // "Your operation could not succeed because field {0} cannot link to non-existent invid {1}.\n\nThis is a serious logic error in the server." return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_newref_sub"), ts.l("bind.no_newref", getName(), newRemote.toString())); } // see if we are allowed to link the remote object without having // permission to edit it generally. anonymous2 = session.getObjectHook(newRemote).anonymousLinkOK(remobj, targetField, this.owner, this.getID(), getGSession()); // if we're already editing it, just go with that. if (remobj instanceof DBEditObject) { newRef = (DBEditObject) remobj; // if the object is being deleted or dropped, don't allow the link if (newRef.getStatus() == ObjectStatus.DELETING || newRef.getStatus() == ObjectStatus.DROPPING) { // "InvidDBField.bind(): Couldn''t link to remote object" // "Field {0} cannot be linked to remote object {1}.\n\nThe remote object has been deleted." return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_new_link_sub"), ts.l("bind.deleted_new", this.getName(), newRemote.toString())); } } else { if (anonymous2 || getGSession().getPermManager().getPerm(remobj).isEditable()) { newRef = session.editDBObject(newRemote); } else { // "InvidDBField.bind(): Couldn''t link to remote object" // "Field {0} could not be linked to the {1} {2} object. You do not have permission to edit the {1} {2} object." return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_new_link_sub"), ts.l("bind.no_newref_perm", this.getName(), remobj.getLabel(), remobj.getTypeName())); } } // if we couldn't check out the object, we need to see why. if (newRef == null) { // this section is not truly thread safe, as the shadowObject // might be cleared by another thread while we're working.. // this isn't a grave risk, but we'll wrap all of this error // message logic in a NullPointerException catch just in case. try { String edit_username, edit_hostname; DBEditObject editing = remobj.getShadow(); if (editing != null) { if (editing.gSession != null) { edit_username = editing.gSession.getPermManager().getBaseIdentity(); edit_hostname = editing.gSession.getClientHostName(); // "InvidDBField.bind(): Couldn''t link to new reference" // "Field {0} could not be linked to the {1} {2} object, which is busy being edited by {3} on system {4}." return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_new_link_sub"), ts.l("bind.busy_new", this.getName(), remobj.getLabel(), remobj.getTypeName(), edit_username, edit_hostname)); } // "InvidDBField.bind(): Couldn''t link to new reference" // "Field {0} could not be linked to the {1} {2} object, which is busy being edited by another user." return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_new_link_sub"), ts.l("bind.busy_new2", this.getName(), remobj.getLabel(), remobj.getTypeName())); } else { // "InvidDBField.bind(): Couldn''t link to new reference" // "Field {0} could not be linked to the {1} {2} object. // This is probably a temporary condition due to other user activity on the Ganymede server." return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_new_link_sub"), ts.l("bind.busy_new_temp", this.getName(), remobj.getLabel(), remobj.getTypeName())); } } catch (NullPointerException ex) { // "InvidDBField.bind(): Couldn''t link to new reference" // "Field {0} could not be linked to the {1} {2} object. // This is probably a temporary condition due to other user activity on the Ganymede server." return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_new_link_sub"), ts.l("bind.busy_new_temp", this.getName(), remobj.getLabel(), remobj.getTypeName())); } } try { newRefField = newRef.getInvidField(targetField); if (newRefField == null) { // "InvidDBField.bind(): Couldn''t link to new reference" // "Your operation could not succeed due to a possible inconsistency in the server database. Target field number {0} in object {1} does not exist." return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_new_link_sub"), ts.l("bind.inconsistency", Integer.toString(targetField), newRef.getLabel())); } } catch (ClassCastException ex) { // "InvidDBField.bind(): Couldn''t link to new reference" // "Your operation could not succeed due to an error in the server''s schema. Target field {0} in object {1} is not an invid field." return Ganymede.createErrorDialog(this.getGSession(), ts.l("bind.no_new_link_sub"), ts.l("bind.schema_error", newRef.getField(targetField).getName(), newRef.getLabel())); } // okay, at this point we should have oldRefField pointing to the // old target field, and newRefField pointing to the new target field. // Do our job. if (oldRefField != null) { retVal = oldRefField.dissolve(this.owner.getInvid(), (anonymous||local)); if (!ReturnVal.didSucceed(retVal)) { return retVal; } } retVal = ReturnVal.merge(retVal, newRefField.establish(this.owner, (anonymous2||local))); if (!ReturnVal.didSucceed(retVal)) { // oops! try to undo what we did.. this probably isn't critical // because something above us will do a rollback, but it's polite. if (oldRefField != null) { oldRefField.establish(this.owner, (anonymous||local)); // hope this works } return retVal; } // tell the client that it needs to rescan both the old and new // remote ends of this binding retVal = ReturnVal.merge(retVal, ReturnVal.success()); // force non-null ReturnVal if (oldRemote != null) { retVal.addRescanField(oldRemote, targetField); } retVal.addRescanField(newRemote, targetField); return retVal; // success } /** * <p>This method is used to unlink this field from the specified remote * invid in accordance with this field's defined symmetry constraints.</p> * * <p>This method deals with the back pointer (symmetric or * asymmetric), the forward link is broken in InvidDBField through * direct manipulation of the field's value.</p> * * <p><b>This method is private, and is not to be called by any code outside * of this class.</b></p> * * @param remote An invid for an object to be checked out and unlinked * @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 unbind(Invid remote, boolean local) { short targetField; DBEditObject eObj = null, oldRef = null; DBObject remobj; InvidDBField oldRefField = null; DBSession session = null; ReturnVal retVal = null, newRetVal; boolean anonymous; /* -- */ if (remote == null) { return null; // throw new IllegalArgumentException("null remote: " + getName() + " in object " + this.owner.getLabel()); } if (!isEditable(local)) { // "Not an editable invid field: {0} in object {1}" throw new IllegalArgumentException(ts.l("unbind.noteditable", getName(), this.owner.getLabel())); } if (debug) { System.err.println("InvidDBField[" + toString() + "].unbind(" + remote + ", " + local + ")"); } eObj = (DBEditObject) this.owner; session = eObj.getDBSession(); if (!getFieldDef().isSymmetric()) { unlinkTarget(remote); return null; } // find out what field in remote we might need to update targetField = getFieldDef().getTargetField(); // 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 anonymous variable to instruct dissolve to disregard // write permissions if we have gotten the anonymous OK remobj = session.viewDBObject(remote); if (remobj == null) { // "InvidDBField.unbind(): Couldn't find old reference" // "Your operation could not succeed because field {0} was linked to a remote reference {1} that could not be found // for unlinking.\n\nThis is a serious logic error in the server." return Ganymede.createErrorDialog(this.getGSession(), ts.l("unbind.no_oldref"), ts.l("bind.no_oldref_text", getName(), remote.toString())); } // see if we are allowed to unlink the remote object without // having permission to edit it generally. anonymous = session.getObjectHook(remote).anonymousUnlinkOK(remobj, targetField, this.owner, this.getID(), getGSession()); // if we're already editing it, just go with that. if (remobj instanceof DBEditObject) { oldRef = (DBEditObject) remobj; } else { if (anonymous || getGSession().getPermManager().getPerm(remobj).isEditable()) { oldRef = session.editDBObject(remote); // if we couldn't checkout the old object for editing, despite // having permissions, we need to see why. if (oldRef == null) { // this check is not truly thread safe, as the // shadowObject might be cleared by another thread while // we're working.. this isn't a grave risk, but we'll // wrap all of this in a NullPointerException catch just // in case. this check is just for informative purposes, // so we don't mind throwing a null pointer exception in // here, it's not worth doing all of the careful sync work // to lock down this stuff without risk of deadlock try { String edit_username, edit_hostname; DBEditObject editing = remobj.getShadow(); edit_username = editing.gSession.getPermManager().getBaseIdentity(); edit_hostname = editing.gSession.getClientHostName(); // "InvidDBField.unbind(): Couldn''t unlink from old reference" // "Field {0} could not be unlinked from the {1} {2} object, which is busy being edited by {3} on system {4}." return Ganymede.createErrorDialog(this.getGSession(), ts.l("unbind.no_unlink_sub"), ts.l("bind.busy_old", this.getName(), remobj.getLabel(), remobj.getTypeName(), edit_username, edit_hostname)); } catch (NullPointerException ex) { // "InvidDBField.unbind(): Couldn''t unlink from old reference" // "Field {0} could not be unlinked from the {1} {2} object. // This is probably a temporary condition due to other user activity on the Ganymede server." return Ganymede.createErrorDialog(this.getGSession(), ts.l("unbind.no_unlink_sub"), ts.l("bind.busy_old_temp", this.getName(), remobj.getLabel(), remobj.getTypeName())); } } } else { // it's there, but we don't have permission to unlink it // "InvidDBField.unbind(): Couldn''t unlink from old reference" // "We couldn''t unlink field {0} in object {1} from field {2} in object {3} due to a permissions problem." return Ganymede.createErrorDialog(this.getGSession(), ts.l("unbind.no_unlink_sub"), ts.l("unbind.perm_fail", getName(), this.owner.getLabel(), remobj.getFieldName(targetField), getRemoteLabel(getGSession(), remote, false))); } } try { oldRefField = oldRef.getInvidField(targetField); } catch (ClassCastException ex) { // "InvidDBField.unbind(): Couldn''t unlink from old reference" // "Your operation could not succeed due to an error in the server''s schema. Target field {0} in object {1} is not an invid field." return Ganymede.createErrorDialog(this.getGSession(), ts.l("unbind.no_unlink_sub"), ts.l("bind.schema_error", oldRef.getFieldName(targetField), oldRef.getLabel())); } if (oldRefField == null) { // editDBObject() will create undefined fields for all // fields defined in the DBObjectBase as long as the user // had permission to create those fields, so if we got a // null result we either have a schema corruption problem // or a permission to create field problem DBObjectBaseField fieldDef = oldRef.getFieldDef(targetField); if (fieldDef == null) { // "InvidDBField.unbind(): Couldn''t unlink from old reference" // "Your operation could not succeed due to a possible inconsistency in the server database. Target field number {0} in object {1} does not exist." return Ganymede.createErrorDialog(this.getGSession(), ts.l("unbind.no_unlink_sub"), ts.l("bind.inconsistency", Integer.toString(targetField), oldRef.getLabel())); } else { // "InvidDBField.unbind(): Couldn''t unlink from old reference" // "Your operation could not succeed due to a possible inconsistency in the server database. Target field {0} is undefined in object {1}." return Ganymede.createErrorDialog(this.getGSession(), ts.l("unbind.no_unlink_sub"), ts.l("bind.inconsistency", fieldDef.getName(), oldRef.getLabel())); } } try { // note that we only want to remove one instance of the invid // pointing back to us.. we may have multiple fields on // this object pointing to the remote, and we want to only // clear one back pointer at a time. retVal = oldRefField.dissolve(this.owner.getInvid(), anonymous||local); if (!ReturnVal.didSucceed(retVal)) { return retVal; } } catch (IllegalArgumentException ex) { System.err.println("hm, couldn't dissolve a reference in " + getName()); if (anonymous) { System.err.println("Did do an anonymous edit on target"); } else { System.err.println("Didn't do an anonymous edit on target"); } throw (IllegalArgumentException) ex; } // tell the client that it needs to rescan the old remote end of this binding newRetVal = new ReturnVal(true, true); newRetVal.addRescanField(remote, targetField); newRetVal.unionRescan(retVal); return newRetVal; // success } /** * <p>This method is used to effect the remote side of a unbind * operation on a symmetric link.</p> * * <p>An InvidDBField being manipulated with the standard editing accessors * (setValue, addElement, deleteElement, setElement) will call this method * on another InvidDBField in order to unlink a pair of symmetrically bound * InvidDBFields.</p> * * <p>This method will return false if the unbinding could not be performed for * some reason.</p> * * <p>This method is private, and is not to be called by any code outside * of this class.</p> * * @param oldInvid The invid to be unlinked from this field. If this * field is not linked to the invid specified, nothing will happen. * @param local if true, this operation will be performed without regard * to permissions limitations. * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ synchronized final ReturnVal dissolve(Invid oldInvid, boolean local) { DBEditObject eObj; /* -- */ // We wouldn't be called here unless this Object and InvidDBField // were editable.. bind/unbind check things out for us. eObj = (DBEditObject) this.owner; if (isVector()) { Vector<Invid> values = (Vector<Invid>) this.getVectVal(); for (int i = 0; i < values.size(); i++) { if (!oldInvid.equals(values.get(i))) { continue; } ReturnVal retVal = eObj.finalizeDeleteElement(this, i); if (ReturnVal.didSucceed(retVal)) { // we got the okay, so we are going to take out this // element and return. note that if we didn't return // here, we might get confused on our values loop. values.remove(i); this.qr = null; return retVal; } else { if (retVal.getDialog() != null) { // "InvidDBField.dissolve(): couldn''t finalizeDeleteElement" // "The custom plug-in class for object {0} refused to allow us to clear out all the references in field {1}:\n\n{2}" return Ganymede.createErrorDialog(this.getGSession(), ts.l("dissolve.no_finalize_vect"), ts.l("dissolve.refused_vect", eObj.getLabel(), getName(), retVal.getDialog().getText())); } else { // "InvidDBField.dissolve(): couldn''t finalizeDeleteElement" // "The custom plug-in class for object {0} refused to allow us to clear out all the references in field {1}" return Ganymede.createErrorDialog(this.getGSession(), ts.l("dissolve.no_finalize_vect"), ts.l("dissolve.refused_vect_notext", eObj.getLabel(), getName())); } } } // "Warning: dissolve for {0}:{1} called with an unbound invid {2}" Ganymede.debug(ts.l("dissolve.unbound_vector", this.owner.getLabel(), getName(), oldInvid.toString())); return null; // we're already dissolved, effectively } else { Invid tmp = (Invid) value; if (!tmp.equals(oldInvid)) { // Warning: dissolve for {0}:{1} called with an unbound invid {2}, current value = {3} throw new RuntimeException(ts.l("dissolve.unbound_scalar", this.owner.getLabel(), getName(), oldInvid, tmp)); } ReturnVal retVal = eObj.finalizeSetValue(this, null); if (ReturnVal.didSucceed(retVal)) { this.value = null; this.qr = null; return retVal; } else { if (retVal.getDialog() != null) { // "InvidDBField.dissolve(): couldn''t finalizeSetValue" // "The custom plug-in class for object {0} refused to allow us to clear out the reference in field {1}:\n\n{2}" return Ganymede.createErrorDialog(this.getGSession(), ts.l("dissolve.no_finalize_scalar"), ts.l("dissolve.refused_scalar", eObj.getLabel(), getName(), retVal.getDialog().getText())); } else { // "InvidDBField.dissolve(): couldn''t finalizeSetValue" // "The custom plug-in class for object {0} refused to allow us to clear out the reference in field {1}" return Ganymede.createErrorDialog(this.getGSession(), ts.l("dissolve.no_finalize_scalar"), ts.l("dissolve.refused_scalar_notext", eObj.getLabel(), getName())); } } } } /** * <p>This method is used to effect the remote side of a bind operation * on a symmetric link.</p> * * <p>An InvidDBField being manipulated with the standard editing accessors * (setValue, addElement, deleteElement, setElement) will call this method * on another InvidDBField in order to link a pair of symmetrically bound * InvidDBFields.</p> * * <p>This method will return false if the binding could not be performed for * some reason.</p> * * <p><b>This method is private, and is not to be called by any code outside * of this class.</b></p> * * @param newObject The DBObject whose Invid is to be linked to this field. * @param local if true, this operation will be performed without regard * to permissions limitations. * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ private synchronized final ReturnVal establish(DBObject newObject, boolean local) { Invid tmp = null; DBEditObject eObj; ReturnVal retVal = null; Invid newInvid = newObject.getInvid(); /* -- */ // We wouldn't be called here unless this Object and InvidDBField // were editable.. bind checks things out for us. eObj = (DBEditObject) this.owner; if (eObj.getStatus() == ObjectStatus.DELETING || eObj.getStatus() == ObjectStatus.DROPPING) { // "InvidDBField.establish(): can''t link to deleted object" // "Couldn''t establish a new linkage in field {0} because object {1} has been deleted." return Ganymede.createErrorDialog(this.getGSession(), ts.l("establish.deletion_sub"), ts.l("establish.deletion_text", this.getName(), this.owner.getLabel())); } if (isVector()) { if (size() >= getMaxArraySize()) { // "InvidDBField.establish(): Can''t link to full field" // "Couldn''t establish a new linkage in vector field {0} in object {1} because the vector field is already at maximum capacity." return Ganymede.createErrorDialog(this.getGSession(), ts.l("establish.overrun_sub"), ts.l("establish.overrun_text", getName(), this.owner.getLabel())); } Vector<Invid> values = (Vector<Invid>) this.getVectVal(); // For everybody else, though, this is a no-no. if (values.contains(newInvid)) { // "InvidDBField.establish(): Schema logic error" // "The reverse link field field {0} in object {1} refused the pointer binding // because it already points back to the object requesting binding. This sugests that multiple fields in the originating // object {2} {3} are trying to link to one vector field in we, the target, which can''t work. If one of the fields in {3} // were ever cleared or changed, we''d be cleared and the symmetric relationship would be broken.\n\n // Have your adopter check the schema." return Ganymede.createErrorDialog(this.getGSession(), ts.l("establish.schema_sub"), ts.l("establish.schema_text", getName(), this.owner.getLabel(), newObject.getTypeName(), newObject.getLabel())); } retVal = eObj.finalizeAddElement(this, newInvid); if (!ReturnVal.didSucceed(retVal)) { if (retVal.getDialog() != null) { // "InvidDBField.establish(): finalizeAddElement refused" // "Couldn''t establish a new linkage in vector field {0} in object {1} because the custom plug in code // for this object refused to approve the operation:\n\n{2}" return Ganymede.createErrorDialog(this.getGSession(), ts.l("establish.no_add_sub"), ts.l("establish.no_add_text", getName(), this.owner.getLabel(), retVal.getDialog().getText())); } else { // "InvidDBField.establish(): finalizeAddElement refused" // "Couldn''t establish a new linkage in vector field {0} in object {1} because the custom plug in code // for this object refused to approve the operation." return Ganymede.createErrorDialog(this.getGSession(), ts.l("establish.no_add_sub"), ts.l("establish.no_add_text2", getName(), this.owner.getLabel())); } } else { values.add(newInvid); this.qr = null; return retVal; } } else { // ok, since we're scalar, *we* need to be unbound from *our* // existing target in order to be free to point back to our // friend who is trying to establish a link to us if (value != null) { tmp = (Invid) value; if (tmp.equals(newInvid)) { // "InvidDBField.establish(): schema logic error" // "The reverse link field field {0} in object {1} refused the pointer binding // because it already points back to the object requesting binding. This sugests that multiple fields in the originating // object {2} {3} are trying to link to one scalar field in we, the target, which can''t work. If one of the fields in {3} // were ever cleared or changed, we''d be cleared and the symmetric relationship would be broken.\n\n // Have your adopter check the schema." return Ganymede.createErrorDialog(this.getGSession(), ts.l("establish.schema_sub"), ts.l("establish.schema_scalar_text", getName(), this.owner.getLabel(), newObject.getTypeName(), newObject.getLabel())); } retVal = unbind(tmp, local); if (!ReturnVal.didSucceed(retVal)) { return retVal; } } retVal = ReturnVal.merge(retVal, eObj.finalizeSetValue(this, newInvid)); if (ReturnVal.didSucceed(retVal)) { value = newInvid; this.qr = null; return retVal; } else { if (!ReturnVal.didSucceed(bind(null, tmp, local))) // bind back to the original target, should always work { throw new RuntimeException("couldn't rebind a value " + tmp + " we just unbound.. sync error"); } if (retVal.getDialog() != null) { // "InvidDBField.establish(): finalizeSetValue refused" // "Couldn''t establish a new linkage in field {0} in object {1} because the custom plug in code // for this object refused to approve the operation:\n\n{2}" return Ganymede.createErrorDialog(this.getGSession(), ts.l("establish.no_set_sub"), ts.l("establish.no_set_text", getName(), this.owner.getLabel(), retVal.getDialog().getText())); } else { // "InvidDBField.establish(): finalizeSetValue refused" // "Couldn''t establish a new linkage in field {0} in object {1} because the custom plug in code // for this object refused to approve the operation." return Ganymede.createErrorDialog(this.getGSession(), ts.l("establish.no_set_sub"), ts.l("establish.no_set_text2", getName(), this.owner.getLabel())); } } } } /** * <p>This test method checks to see if the invid's held in this * InvidDBField point to existing objects and are properly * back-referenced.</p> * * <p>It is typically triggered from the admin console's "Integrity * Test" command in the console's debug menu.</p> */ synchronized boolean test(DBSession session, String objectName) { Invid myInvid = this.owner.getInvid(); short targetField; DBObject target; InvidDBField backField; boolean asymBackPointer; boolean result = true; /* -- */ if (getFieldDef().isSymmetric()) { targetField = getFieldDef().getTargetField(); asymBackPointer = false; } else { asymBackPointer = true; // we'll test Ganymede.db.aSymLinkTracker targetField = -1; } if (isVector()) { Vector<Invid> values = (Vector<Invid>) getVectVal(); // test for all values in our vector for (Invid temp: values) { if (asymBackPointer) { target = session.viewDBObject(temp); if (target == null) { // "*** InvidDBField.test(): Invid pointer to null object {0} located: {1} in field {2}" Ganymede.debug(ts.l("test.pointer_to_null_object", temp, objectName, getName())); result = false; continue; } if (!Ganymede.db.aSymLinkTracker.linkExists(getDBSession(), myInvid, temp)) { // "*** InvidDBField.test(): aSymLinkTracker doesn''t contain {0} for Invid {1} pointed to from {2} in field {3}" Ganymede.debug(ts.l("test.no_contains", myInvid, temp, objectName, getName())); result = false; continue; } } else { // find the object that this invid points to target = session.viewDBObject(temp); if (target == null) { // "*** InvidDBField.test(): Invid pointer to null object {0} located: {1} in field {2}" Ganymede.debug(ts.l("test.pointer_to_null_object", temp, objectName, getName())); result = false; continue; } // find the field that should contain the back-pointer try { backField = target.getInvidField(targetField); } catch (ClassCastException ex) { String fieldName = target.getField(targetField).getName(); // "*** InvidDBField.test(): schema error! back-reference field not an invid field!!\n\t>{0}:{1}, referenced from {2}:{3}" Ganymede.debug(ts.l("test.bad_symmetry", target.getLabel(), fieldName, objectName, getName())); result = false; continue; } if (backField == null) { // "InvidDBField.test(): Object {0}, field {1} is targeting a field, {2} in object {3} which does not exist!" Ganymede.debug(ts.l("test.pointer_to_null_field", objectName, getName(), Integer.toString(targetField), target)); result = false; continue; } else if (!backField.isDefined()) { // "InvidDBField.test(): Object {0}, field {1} is targeting a field, {2} in object {3} which is not defined!" Ganymede.debug(ts.l("test.pointer_to_undefined_field", objectName, getName(), backField.getName(), target)); result = false; continue; } if (backField.isVector()) { if (backField.getVectVal() == null) { // "*** InvidDBField.test(): Null back-link invid found for invid {0} in object {1} in field {2}" Ganymede.debug(ts.l("test.empty_backlink", temp, objectName, getName())); result = false; continue; } else { Vector<Invid> backValues = (Vector<Invid>) backField.getVectVal(); /* -- */ if (!backValues.contains(myInvid)) { // "*** InvidDBField.test(): No back-link invid found for invid {0} in object {1}:{2} in {3}" Ganymede.debug(ts.l("test.no_symmetry", temp, objectName, getName(), backField.getName())); result = false; continue; } } } else { if ((backField.value == null) || !(backField.value.equals(myInvid))) { // "*** InvidDBField.test(): No back-link invid found for invid {0} in object {1}:{2} in {3}" Ganymede.debug(ts.l("test.no_symmetry", temp, objectName, getName(), backField.getName())); result = false; continue; } } } } } else // scalar invid field case { Invid temp = (Invid) value; if (asymBackPointer) { target = session.viewDBObject(temp); if (target == null) { // "*** InvidDBField.test(): Invid pointer to null object {0} located: {1} in field {2}" Ganymede.debug(ts.l("test.pointer_to_null_object", temp, objectName, getName())); result = false; } if (!Ganymede.db.aSymLinkTracker.linkExists(getDBSession(), myInvid, temp)) { // "*** InvidDBField.test(): backpointer hash doesn''t contain {0} for Invid {1} pointed to from {2} in field {3}" Ganymede.debug(ts.l("test.no_contains", myInvid, temp, objectName, getName())); result = false; } } else { if (temp != null) { target = session.viewDBObject(temp); if (target == null) { Ganymede.debug(ts.l("test.pointer_to_null_object", temp, objectName, getName())); return false; } try { backField = target.getInvidField(targetField); } catch (ClassCastException ex) { String fieldName = target.getField(targetField).getName(); Ganymede.debug(ts.l("test.bad_symmetry", target.getLabel(), fieldName, objectName, getName())); return false; } if (backField == null) { Ganymede.debug(ts.l("test.pointer_to_null_field", objectName, getName(), Integer.toString(targetField), target)); return false; } else if (!backField.isDefined()) { Ganymede.debug(ts.l("test.pointer_to_undefined_field", objectName, getName(), backField.getName(), target)); return false; } if (backField.isVector()) { Vector<Invid> backValues = (Vector<Invid>) backField.getVectVal(); if (backValues == null) { Ganymede.debug(ts.l("test.empty_backlink", temp, objectName, getName())); return false; } else { if (!backValues.contains(myInvid)) { Ganymede.debug(ts.l("test.no_symmetry", temp, objectName, getName(), backField.getName())); return false; } } } else { if ((backField.value == null) || !(backField.value.equals(myInvid))) { Ganymede.debug(ts.l("test.no_symmetry", temp, objectName, getName(), backField.getName())); return false; } } } } } return result; } // **** // // InvidDBField is a special kind of DBField in that we have symmetry // maintenance issues to handle. We're overriding all DBField field-changing // methods to include symmetry maintenance code. // // **** /** * <p>Sets the value of this field, if a scalar.</p> * * <p>The Invid we are passed must refer to a valid object in the * database. The remote object will be checked out for * editing and a backpointer will placed in it. If this field * previously held a pointer to another object, that other * object will be checked out and its pointer to us cleared.</p> * * <p>The ReturnVal object returned encodes success or failure, and may * optionally pass back a dialog.</p> * * @param value the value to set this field to, and Invid * @param local if true, this operation will be performed without regard * to permissions limitations. * @param noWizards If true, wizards will be skipped * * @see arlut.csd.ganymede.server.DBSession * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ @Override public synchronized ReturnVal setValue(Object value, boolean local, boolean noWizards) { DBEditObject eObj; Invid oldRemote, newRemote; ReturnVal retVal = null; String checkkey = null; boolean checkpointed = false; /* -- */ if (!isEditable(local)) { // "Don''t have permission to change field {0} in object {1}" return Ganymede.createErrorDialog(this.getGSession(), "InvidDBField.setValue()", ts.l("global.no_perms", getName(), this.owner.getLabel())); } if (isVector()) { // "Scalar method called on a vector field: {0} in object {1}" throw new IllegalArgumentException(ts.l("global.oops_vector", getName(), this.owner.getLabel())); } if ((this.value == null && value == null) || (this.value != null && this.value.equals(value))) { if (debug) { Ganymede.debug("InvidDBField.setValue(): no change"); } return null; // no change } retVal = verifyNewValue(value, local); if (!ReturnVal.didSucceed(retVal)) { return retVal; } // we now know that value is an invid oldRemote = (Invid) this.value; newRemote = (Invid) value; eObj = (DBEditObject) this.owner; if (!noWizards && !local && getGSession().enableOversight) { // Wizard check retVal = ReturnVal.merge(retVal, eObj.wizardHook(this, DBEditObject.SETVAL, value, null)); // if a wizard intercedes, we are going to let it take the ball. if (ReturnVal.wizardHandled(retVal)) { return retVal; } } checkkey = RandomUtils.getSaltedString("setValue[" + getName() + ":" + this.owner.getLabel() + "]"); eObj.getDBSession().checkpoint(checkkey); // may block if another thread has checkpointed this transaction checkpointed = true; try { // try to do the binding if (newRemote != null) { retVal = ReturnVal.merge(retVal, bind(oldRemote, newRemote, local)); } else if (oldRemote != null) { retVal = ReturnVal.merge(retVal, unbind(oldRemote, local)); } if (!ReturnVal.didSucceed(retVal)) { return retVal; } // check our owner, do it. Checking our owner should // be the last thing we do.. if it returns true, nothing // should stop us from running the change to completion retVal = ReturnVal.merge(retVal, eObj.finalizeSetValue(this, value)); if (ReturnVal.didSucceed(retVal)) { this.value = value; this.qr = null; // success! eObj.getDBSession().popCheckpoint(checkkey); checkpointed = false; } return retVal; } finally { if (checkpointed) { eObj.getDBSession().rollback(checkkey); } } } /** * <p>Sets the value of an element of this field, if a vector.</p> * * <p>The Invid we are passed must refer to a valid object in the * database. The remote object will be checked out for * editing and a backpointer will placed in it. If this field * previously held a pointer to another object, that other * object will be checked out and its pointer to us cleared.</p> * * <p>The ReturnVal object returned encodes success or failure, and may * optionally pass back a dialog.</p> * * <p>It is an error to call this method on an edit in place vector, * or on a scalar field. An IllegalArgumentException will be thrown * in these cases.</p> * * @param index The index of the element in this field to change. * @param submittedValue The value to put into this vector. * @param local if true, this operation will be performed without regard * to permissions limitations. * @param noWizards If true, wizards will be skipped * * @see arlut.csd.ganymede.server.DBSession * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ @Override public synchronized ReturnVal setElement(int index, Object submittedValue, boolean local, boolean noWizards) { DBEditObject eObj; ReturnVal retVal = null; String checkkey = null; boolean checkpointed = false; /* -- */ // DBField.setElement(), DBField.setElementLocal() check the index and value // params for us. if (!isVector()) { throw new IllegalArgumentException(ts.l("global.oops_scalar", getName(), this.owner.getLabel())); } if (isEditInPlace()) { // "Can''t manually set element in edit-in-place vector: {0} in object {1}" throw new IllegalArgumentException(ts.l("setElement.edit_in_place", getName(), this.owner.getLabel())); } if (!isEditable(local)) { return Ganymede.createErrorDialog(this.getGSession(), "InvidDBField.setElement()", ts.l("global.no_perms", getName(), this.owner.getLabel())); } Vector<Invid> values = (Vector<Invid>) getVectVal(); Invid oldRemote = values.get(index); Invid newRemote = (Invid) submittedValue; int oldIndex = values.indexOf(newRemote); if (oldIndex == index) { return null; // no-op } else if (oldIndex != -1) { return getDuplicateValueDialog("setElement", newRemote); // duplicate } retVal = verifyNewValue(newRemote, local); if (!ReturnVal.didSucceed(retVal)) { return retVal; } eObj = (DBEditObject) this.owner; if (!local && getGSession().enableOversight) { // Wizard check retVal = ReturnVal.merge(retVal, eObj.wizardHook(this, DBEditObject.SETELEMENT, Integer.valueOf(index), newRemote)); // if a wizard intercedes, we are going to let it take the ball. if (ReturnVal.wizardHandled(retVal)) { return retVal; } } checkkey = RandomUtils.getSaltedString("setElement[" + getName() + ":" + eObj.getLabel() + "]"); eObj.getDBSession().checkpoint(checkkey); // may block if another thread has checkpoint this transaction checkpointed = true; try { // try to do the binding retVal = ReturnVal.merge(retVal, bind(oldRemote, newRemote, local)); if (!ReturnVal.didSucceed(retVal)) { return retVal; } // Let our owner check things out with the finalizeSetElement call. // // Checking our owner should be the last thing we do.. if it // returns true, nothing should stop us from running the // change to completion retVal = ReturnVal.merge(retVal, eObj.finalizeSetElement(this, index, newRemote)); if (ReturnVal.didSucceed(retVal)) { values.set(index, newRemote); this.qr = null; // success! eObj.getDBSession().popCheckpoint(checkkey); checkpointed = false; } return retVal; } finally { if (checkpointed) { eObj.getDBSession().rollback(checkkey); } } } /** * <p>Adds an element to the end of this field, if a vector.</p> * * <p>The Invid we are passed must refer to a valid object in the * database. The remote object will be checked out for * editing and a backpointer will placed in it. If this field * previously held a pointer to another object, that other * object will be checked out and its pointer to us cleared.</p> * * <p>The ReturnVal object returned encodes success or failure, and may * optionally pass back a dialog.</p> * * <p>It is an error to call this method on an edit in place vector, * or on a scalar field. An IllegalArgumentException will be thrown * in these cases.</p> * * @param submittedValue The value to put into this vector. * @param local if true, this operation will be performed without regard * to permissions limitations. * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ @Override public synchronized ReturnVal addElement(Object submittedValue, boolean local, boolean noWizards) { DBEditObject eObj; Invid remote; ReturnVal retVal = null; String checkkey = null; boolean checkpointed = false; /* -- */ if (!isEditable(local)) // *sync* on GanymedeSession possible { return Ganymede.createErrorDialog(this.getGSession(), "InvidDBField.addElement()", ts.l("global.no_perms", getName(), this.owner.getLabel())); } if (isEditInPlace()) { return Ganymede.createErrorDialog(this.getGSession(), "InvidDBField.addElement()", ts.l("addElement.edit_in_place", getName(), this.owner.getLabel())); } if (!isVector()) { throw new IllegalArgumentException(ts.l("global.oops_scalar", getName(), this.owner.getLabel())); } Vector values = getVectVal(); // don't both adding something we've already got if (values.contains(submittedValue)) { return getDuplicateValueDialog("addElement", submittedValue); // duplicate } retVal = verifyNewValue(submittedValue, local); if (!ReturnVal.didSucceed(retVal)) { return retVal; } if (size() >= getMaxArraySize()) { // "InvidDBField.addElement() - vector overflow" // "Field {0} already at or beyond array size limit." return Ganymede.createErrorDialog(this.getGSession(), ts.l("addElement.overflow_sub"), ts.l("addElement.overflow_text", getName())); } remote = (Invid) submittedValue; eObj = (DBEditObject) this.owner; if (!noWizards && !local && getGSession().enableOversight) { // Wizard check retVal = ReturnVal.merge(retVal, eObj.wizardHook(this, DBEditObject.ADDELEMENT, submittedValue, null)); // if a wizard intercedes, we are going to let it take the ball. if (ReturnVal.wizardHandled(retVal)) { return retVal; } } // check to make sure that we're not trying to add a remote // reference that we can't safely link to without breaking a // symmetric relationship. retVal = ReturnVal.merge(retVal, checkBindConflict(remote)); if (!ReturnVal.didSucceed(retVal)) { return retVal; } checkkey = RandomUtils.getSaltedString("addElement[" + getName() + ":" + this.owner.getLabel() + "]"); eObj.getDBSession().checkpoint(checkkey); // may block if another thread has already checkpointed this transaction checkpointed = true; try { retVal = ReturnVal.merge(retVal, bind(null, remote, local)); if (!ReturnVal.didSucceed(retVal)) { return retVal; } retVal = ReturnVal.merge(retVal, eObj.finalizeAddElement(this, submittedValue)); if (ReturnVal.didSucceed(retVal)) { values.add(submittedValue); this.qr = null; // success! eObj.getDBSession().popCheckpoint(checkkey); checkpointed = false; } return retVal; } finally { if (checkpointed) { eObj.getDBSession().rollback(checkkey); } } } /** * <p>Adds a set of elements to the end of this field, if a * vector. Using addElements() to add a sequence of items * to a field may be many times more efficient than calling * addElement() repeatedly, as addElements() can do a single * server checkpoint before attempting to add all the values.</p> * * <p>The Invid we are passed must refer to a valid object in the * database. The remote object will be checked out for * editing and a backpointer will placed in it. If this field * previously held a pointer to another object, that other * object will be checked out and its pointer to us cleared.</p> * * <p>It is an error to call this method on an edit in place vector, * or on a scalar field. An IllegalArgumentException will be thrown * in these cases.</p> * * <p>Server-side method only</p> * * <p>The ReturnVal object returned encodes success or failure, and * may optionally pass back a dialog.</p> * * @param submittedValues Values to be added * @param local If true, permissions checking will be skipped * @param noWizards If true, wizards will be skipped * @param partialSuccessOk If true, addElements will add any values that * it can, even if some values are refused by the server logic. Any * values that are skipped will be reported in a dialog passed back * in the returned ReturnVal * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ @Override public synchronized ReturnVal addElements(Vector submittedValues, boolean local, boolean noWizards, boolean partialSuccessOk) { boolean success = false; String checkkey = null; ReturnVal retVal = null; DBEditObject eObj; Vector<Invid> values; Vector<Invid> approvedValues = new Vector<Invid>(); Vector<Invid> failed_bindings = null; Vector<Invid> newValues = (Vector<Invid>) submittedValues; /* -- */ if (debug) { System.err.println("InvidDBField.addElements(" + VectorUtils.vectorString(newValues) + ")"); } if (isEditInPlace()) { // "Can''t manually add elements to edit-in-place vector: {0} in object {1}" return Ganymede.createErrorDialog(this.getGSession(), "InvidDBField.addElements()", ts.l("addElements.edit_in_place", getName(), this.owner.getLabel())); } if (!isEditable(local)) // *sync* on GanymedeSession possible { return Ganymede.createErrorDialog(this.getGSession(), "InvidDBField.addElements()", ts.l("global.no_perms", getName(), this.owner.getLabel())); } if (!isVector()) { throw new IllegalArgumentException(ts.l("global.oops_scalar", getName(), this.owner.getLabel())); } values = (Vector<Invid>) getVectVal(); // cast once if (newValues == null || newValues.size() == 0) { return Ganymede.createErrorDialog(this.getGSession(), ts.l("addElements.error_sub"), ts.l("addElements.null_empty_param", getName())); } // can we add this many values? if (size() + newValues.size() > getMaxArraySize()) { return Ganymede.createErrorDialog(this.getGSession(), ts.l("addElements.error_sub"), ts.l("addElements.overflow_text", getName(), Integer.toString(newValues.size()), Integer.toString(size()), Integer.toString(getMaxArraySize()))); } // Don't allow adding values we've already got Vector<Invid> duplicateValues = VectorUtils.intersection(values, newValues); if (duplicateValues.size() > 0) { if (!partialSuccessOk) { return getDuplicateValuesDialog("addElements", VectorUtils.vectorString(duplicateValues)); } else { // we use difference because we know that Ganymede vector // fields are not allowed to contain duplications newValues = VectorUtils.difference(newValues, values); } } // check to see if all of the submitted values are acceptable in // type and in identity. if partialSuccessOk, we won't complain // unless none of the submitted values are acceptable StringBuilder errorBuf = null; for (Invid remote: newValues) { retVal = verifyNewValue(remote, local); if (!ReturnVal.didSucceed(retVal)) { if (!partialSuccessOk) { return retVal; } else { if (retVal.getDialog() != null) { if (errorBuf == null) { errorBuf = new StringBuilder(); } else if (errorBuf.length() != 0) { errorBuf.append("\n\n"); } errorBuf.append(retVal.getDialog().getText()); } } } else { approvedValues.add(remote); } } // if we weren't able to get any copied, report if (approvedValues.size() == 0) { // "AddElements Error" return Ganymede.createErrorDialog(this.getGSession(), ts.l("addElements.subject"), errorBuf.toString()); } // see if our container wants to preemptively intercede in the // adding operation eObj = (DBEditObject) this.owner; if (!noWizards && !local && getGSession().enableOversight) { // Wizard check retVal = eObj.wizardHook(this, DBEditObject.ADDELEMENTS, approvedValues, null); // if a wizard intercedes, we are going to let it take the ball. if (ReturnVal.wizardHandled(retVal)) { return retVal; } } // check to make sure that we're not trying to add a remote // reference that we can't safely link to without breaking a // symmetric relationship. failed_bindings = null; for (Invid remote: approvedValues) { ReturnVal newRetVal = checkBindConflict(remote); if (!ReturnVal.didSucceed(newRetVal)) { if (!partialSuccessOk) { return newRetVal; } else { if (newRetVal.getDialog() != null) { if (errorBuf == null) { errorBuf = new StringBuilder(); } else if (errorBuf.length() != 0) { errorBuf.append("\n\n"); } errorBuf.append(newRetVal.getDialog().getText()); } if (failed_bindings == null) { failed_bindings = new Vector(); } failed_bindings.add(remote); } } } if (failed_bindings != null) { // we use difference because we know that Ganymede vector // fields are not allowed to contain duplications approvedValues = VectorUtils.difference(approvedValues, failed_bindings); } checkkey = RandomUtils.getSaltedString("addElements[" + getName() + ":" + this.owner.getLabel() + "]"); if (debug) { System.err.println("InvidDBField.addElements(): checkpointing " + checkkey); } eObj.getDBSession().checkpoint(checkkey); // may block if another thread has checkpointed this transaction if (debug) { System.err.println("InvidDBField.addElements(): completed checkpointing " + checkkey); } try { if (debug) { System.err.println("InvidDBField.addElements(): binding"); } boolean any_success = false; failed_bindings = null; for (Invid remote: approvedValues) { ReturnVal newRetVal = bind(null, remote, local); // bind us to the target field if (!ReturnVal.didSucceed(newRetVal)) { if (!partialSuccessOk) { return newRetVal; } else { if (newRetVal.getDialog() != null) { if (errorBuf == null) { errorBuf = new StringBuilder(); } else if (errorBuf.length() != 0) { errorBuf.append("\n\n"); } errorBuf.append(newRetVal.getDialog().getText()); } if (failed_bindings == null) { failed_bindings = new Vector(); } failed_bindings.add(remote); } } else { any_success = true; } } if (!any_success) { // "AddElements Error" return Ganymede.createErrorDialog(this.getGSession(), ts.l("addElements.subject"), errorBuf.toString()); } if (failed_bindings != null) { // we use difference because we know that Ganymede vector // fields are not allowed to contain duplications approvedValues = VectorUtils.difference(approvedValues, failed_bindings); } if (debug) { System.err.println("InvidDBField.addElements(): all new elements bound"); } // Okay, see if the DBEditObject is willing to allow all of // these elements to be added. If we're allowing // partialSuccessOk (for cloning), we'll want to query the // DBEditObject about each item to be added, one at a time if (partialSuccessOk) { any_success = false; for (Invid remoteInvid: approvedValues) { ReturnVal newRetVal = eObj.finalizeAddElement(this, remoteInvid); if (ReturnVal.didSucceed(newRetVal)) { values.add(remoteInvid); any_success = true; } else { if (newRetVal.getDialog() != null) { if (errorBuf == null) { errorBuf = new StringBuilder(); } else if (errorBuf.length() != 0) { errorBuf.append("\n\n"); } errorBuf.append(newRetVal.getDialog().getText()); } } } if (!any_success) { // "AddElements Error" return Ganymede.createErrorDialog(this.getGSession(), ts.l("addElements.subject"), errorBuf.toString()); } } else { retVal = ReturnVal.merge(retVal, eObj.finalizeAddElements(this, approvedValues)); if (ReturnVal.didSucceed(retVal)) { if (debug) { System.err.println("InvidDBField.addElements(): finalize approved"); } values.addAll(approvedValues); } else { return retVal; } } this.qr = null; success = true; // if we were not able to copy some of the values (and we // therefore have partialSuccessOk set), encode a description // of what happened to go along with the success code. if (errorBuf != null && errorBuf.length() != 0) { retVal = ReturnVal.merge(retVal, ReturnVal.success()); // force non-null retVal retVal.setDialog(new JDialogBuff("Warning", errorBuf.toString(), Ganymede.OK, // localized ok null, "ok.gif")); } return retVal; } finally { if (success) { if (debug) { System.err.println("InvidDBField.addElements(): popping checkpoint " + checkkey); } eObj.getDBSession().popCheckpoint(checkkey); } else { // undo the bindings if (debug) { System.err.println("InvidDBField.addElements(): rolling back checkpoint " + checkkey); } eObj.getDBSession().rollback(checkkey); } } } /** * <p>Creates and adds a new embedded object in this * field, if it is an edit-in-place vector.</p> * * <p>Returns a {@link arlut.csd.ganymede.common.ReturnVal ReturnVal} which * conveys a success or failure result. If the createNewEmbedded() * call was successful, the ReturnVal will contain * {@link arlut.csd.ganymede.common.Invid Invid} and {@link * arlut.csd.ganymede.rmi.db_object db_object}, which can be retrieved * using the ReturnVal {@link arlut.csd.ganymede.common.ReturnVal#getInvid() getInvid()} * and {@link arlut.csd.ganymede.common.ReturnVal#getObject() getObject()} * methods..</p> * * @see arlut.csd.ganymede.rmi.invid_field * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public ReturnVal createNewEmbedded() throws NotLoggedInException, GanyPermissionsException { return createNewEmbedded(false); } /** * <p>Creates and adds a new embedded object in this field, if it is * an edit-in-place vector.</p> * * <p>Returns a {@link arlut.csd.ganymede.common.ReturnVal ReturnVal} which * conveys a success or failure result. If the createNewEmbedded() * call was successful, the ReturnVal will contain * {@link arlut.csd.ganymede.common.Invid Invid} and {@link * arlut.csd.ganymede.rmi.db_object db_object}, which can be retrieved * using the ReturnVal {@link arlut.csd.ganymede.common.ReturnVal#getInvid() getInvid()} * and {@link arlut.csd.ganymede.common.ReturnVal#getObject() getObject()} * methods.</p> * * @param local If true, we don't check permission to edit this * field before creating the new object. * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ public synchronized ReturnVal createNewEmbedded(boolean local) throws NotLoggedInException, GanyPermissionsException { if (!isEditable(local)) { return Ganymede.createErrorDialog(this.getGSession(), "InvidDBField.createNewEmbedded()", ts.l("global.no_perms", getName(), this.owner.getLabel())); } if (!isVector()) { throw new IllegalArgumentException(ts.l("global.oops_scalar", getName(), this.owner.getLabel())); } if (!isEditInPlace()) { // "Edit-in-place method called on a referential invid field {0} in object {1}" throw new IllegalArgumentException(ts.l("createNewEmbedded.non_embedded", getName(), this.owner.getLabel())); } if (size() >= getMaxArraySize()) { // "Field {0} is already at or beyond the specified array size limit." return Ganymede.createErrorDialog(this.getGSession(), "InvidDBField.createNewEmbedded()", ts.l("addElement.overflow_text", getName())); } Vector values = getVectVal(); DBEditObject eObj = (DBEditObject) this.owner; DBSession session = eObj.getDBSession(); String ckp_label = RandomUtils.getSaltedString("addEmbed[" + eObj.getLabel() + "]"); session.checkpoint(ckp_label); boolean checkpointed = true; try { // have our owner create a new embedded object // for us ReturnVal retVal = eObj.createNewEmbeddedObject(this); if (!ReturnVal.didSucceed(retVal)) { return retVal; } DBEditObject embeddedObj = (DBEditObject) retVal.getObject(); if (embeddedObj == null) { throw new NullPointerException("gah, null embedded obj!"); } // bind the object to its container.. note that ContainerField // is a standard built-in field for embedded objects and as // such it doesn't have the specific details as to the containing // object's binding recorded. We'll have to do the bidirectional // binding ourselves, in two steps. // first, let's do a basic sanity check. i can't imagine this // ever failing, but i've also seen a case of an embedded object // having been set up with a null container value, which shouldn't // be possible either if (eObj.getInvid() == null) { throw new RuntimeException("Error, can't createNewEmbedded with a null owner invid"); } // we have to use setFieldValueLocal() here because the // permissions system uses the ContainerField to determine rights // to modify the field.. since we are just now setting the // container, the permissions system will fail if we don't bypass // it by using the local variant. retVal = embeddedObj.setFieldValueLocal(SchemaConstants.ContainerField, // *sync* DBField eObj.getInvid()); if (!ReturnVal.didSucceed(retVal)) { return retVal; } // finish the binding. Note that we are directly modifying values // here rather than going to this.addElement(). If we did // this.addElement(), we might get a redundant attempt to do the // invid binding, as the containing field may indeed have the // reverse pointer in the object's container field specified in // the schema. Doing it this way, we don't have to worry about // whether the admins got this part of the schema right. if (!local && getGSession().enableOversight) { // Wizard check retVal = eObj.wizardHook(this, DBEditObject.ADDELEMENT, embeddedObj.getInvid(), null); // if a wizard intercedes, we are going to let it take the // ball. note that the wizard had better finish the job it // starts, since we've already done a one-way binding of the // embedded object to its container // // in general, wizardHooks probably shouldn't try to take over // this processing. if (ReturnVal.wizardHandled(retVal)) { session.popCheckpoint(ckp_label); checkpointed = false; return retVal; } } retVal = ReturnVal.merge(retVal, eObj.finalizeAddElement(this, embeddedObj.getInvid())); if (!ReturnVal.didSucceed(retVal)) { return retVal; } values.add(embeddedObj.getInvid()); // do a live modification of this field's invid vector this.qr = null; // record that we created this forward asymmetric link Ganymede.db.aSymLinkTracker.linkObject(getDBSession(), embeddedObj.getInvid(), eObj.getInvid()); // now we need to initialize the new embedded object, since we // defer that activity for embedded objects until after we // get the embedded object linked to the parent retVal = ReturnVal.merge(retVal, embeddedObj.initializeNewObject()); if (!ReturnVal.didSucceed(retVal)) { return retVal; } // sweet, success, forget the checkpoint session.popCheckpoint(ckp_label); checkpointed = false; retVal = ReturnVal.merge(retVal, ReturnVal.success()); // force non-null ReturnVal retVal.setInvid(embeddedObj.getInvid()); retVal.setObject(embeddedObj); return retVal; } finally { // ups, something tanked. rollback if it happened // before we popped if (checkpointed) { session.rollback(ckp_label); } } } /** * <p>Return the object type that this invid field is constrained to point to, if set.</p> * * <p>-1 means there is no restriction on target type.</p> * * <p>-2 means there is no restriction on target type, but there is a specified symmetric field.</p> * * @see arlut.csd.ganymede.rmi.invid_field */ public short getTargetBase() { return getFieldDef().getTargetBase(); } /** * Returns an actual reference to the object type targeted by * this invid field, or null if no specific object type is * targeted. */ public DBObjectBase getTargetBaseDef() { short targetBaseType = getTargetBase(); if (targetBaseType < 0) { return null; } return getFieldDef().base().getStore().getObjectBase(targetBaseType); } /** * Return the numeric id code for the field that this invid field * is set to point to, if any. If -1 is returned, this invid field * does not point to a specific field, and so has no symmetric * relationship. */ public short getTargetField() { return getFieldDef().getTargetField(); } /** * Returns an actual reference to the field definition targeted by * this invid field, or null if no specific field type is * targeted. */ public DBObjectBaseField getTargetFieldDef() { // if we're not pointing to a symmetric field, // return null if (!getFieldDef().isSymmetric()) { return null; } // if we're not pointing to a specific field, also return null. // in practice, this will occur with owner groups whose 'object // owned' field can point to the 'owner' field of any non-embedded // object if (getTargetBase() < 0) { return null; } // we've got a specific field type in a specific object type, find // it DBObjectBase targetBase = getTargetBaseDef(); return targetBase.getField(getTargetField()); } /** * <p>Deletes an element of this field, if a vector.</p> * * <p>The object pointed to by the Invid in the element to be deleted * will be checked out of the database and its pointer to us cleared.</p> * * <p>Returns null on success, non-null on failure.</p> * * <p>If non-null is returned, the ReturnVal object * will include a dialog specification that the * client can use to display the error condition.</p> */ @Override public synchronized ReturnVal deleteElement(int index, boolean local, boolean noWizards) { DBEditObject eObj; Invid remote; ReturnVal retVal = null; String checkkey = null; boolean checkpointed = false; /* -- */ if (!isEditable(local)) { return Ganymede.createErrorDialog(this.getGSession(), "InvidDBField.deleteElement()", ts.l("global.no_perms", getName(), this.owner.getLabel())); } if (!isVector()) { throw new IllegalArgumentException(ts.l("global.oops_scalar", getName(), this.owner.getLabel())); } if (debug) { System.err.println("InvidDBField[" + toString() + ".deleteElement(" + index + ", " + local + ", " + noWizards + ")"); } Vector<Invid> values = (Vector<Invid>) getVectVal(); remote = values.get(index); eObj = (DBEditObject) this.owner; checkkey = RandomUtils.getSaltedString("delElement[" + getName() + ":" + this.owner.getLabel() + "]"); if (!noWizards && !local && getGSession().enableOversight) { // Wizard check retVal = eObj.wizardHook(this, DBEditObject.DELELEMENT, Integer.valueOf(index), null); // if a wizard intercedes, we are going to let it take the ball. if (ReturnVal.wizardHandled(retVal)) { return retVal; } } // ok, we're going to handle it. Checkpoint // so we can easily undo any changes that we make // if we have to return failure. eObj.getDBSession().checkpoint(checkkey); // may block if another thread has checkpointed this transaction checkpointed = true; try { // if we are an edit in place object, we don't want to do an // unbinding.. we'll do a deleteDBObject() below, instead. The // reason for this is that the deleteDBObject() code requires that // the SchemaConstants.ContainerField field be intact to properly // check permissions for embedded objects. if (!getFieldDef().isEditInPlace()) { retVal = ReturnVal.merge(retVal, unbind(remote, local)); if (!ReturnVal.didSucceed(retVal)) { return retVal; } } // finalizeDeleteElement() just gives the DBEditObject a chance to // approve or disapprove deleting an element from this field retVal = ReturnVal.merge(retVal, eObj.finalizeDeleteElement(this, index)); if (ReturnVal.didSucceed(retVal)) { if (getFieldDef().isEditInPlace()) { // We're edit in place. We unlink an embedded object // by deleting it, and letting the asymmetric link // breaking logic in the server remove the deleted // invids out of values for us by way of // DBEditObject.finalizeRemove() and // attemptAsymBackLinkClear(). retVal = ReturnVal.merge(retVal, eObj.getDBSession().deleteDBObject(remote)); if (!ReturnVal.didSucceed(retVal)) { return retVal; // go ahead and return our error code } } else { values.remove(index); } // success this.qr = null; // Clear the cache to force the choices to be read again eObj.getDBSession().popCheckpoint(checkkey); checkpointed = false; return retVal; } else { if (retVal.getDialog() != null) { // "InvidDBField.deleteElement() - custom code rejected element deletion" // "Custom code refused deletion of element {0} from field {1} in object {2}.\n\n{3}" return Ganymede.createErrorDialog(this.getGSession(), ts.l("deleteElement.rejected"), ts.l("deleteElement.no_finalize", Integer.toString(index), getName(), this.owner.getLabel(), retVal.getDialog().getText())); } else { // "InvidDBField.deleteElement() - custom code rejected element deletion" // "Custom code refused deletion of element {0} from field {1} in object {2}." return Ganymede.createErrorDialog(this.getGSession(), ts.l("deleteElement.rejected"), ts.l("deleteElement.no_finalize_no_text", Integer.toString(index), getName(), this.owner.getLabel())); } } } finally { if (checkpointed) { eObj.getDBSession().rollback(checkkey); } } } /** * <p>Removes a set of elements from this field, if a * vector. Using deleteElements() to remove a sequence of items * from a field may be many times more efficient than calling * deleteElement() repeatedly, as removeElements() can do a single * server checkpoint before attempting to remove all the values.</p> * * <p>The ReturnVal object returned encodes success or failure, and * may optionally pass back a dialog.</p> * * <p>Server-side method only</p> * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ @Override public synchronized ReturnVal deleteElements(Vector valuesToDelete, boolean local, boolean noWizards) { ReturnVal retVal = null; String checkkey = null; boolean success = false; DBEditObject eObj; /* -- */ if (!isEditable(local)) { return Ganymede.createErrorDialog(this.getGSession(), "InvidDBField.deleteElements()", ts.l("global.no_perms", getName(), this.owner.getLabel())); } if (!isVector()) { throw new IllegalArgumentException(ts.l("global.oops_scalar", getName(), this.owner.getLabel())); } Vector<Invid> invidsToDelete = (Vector<Invid>) valuesToDelete; Vector<Invid> currentValues = (Vector<Invid>) getVectVal(); // see if we are being asked to remove items not in our vector Vector<Invid> notPresent = VectorUtils.difference(invidsToDelete, currentValues); if (notPresent.size() != 0) { // "Field {0} can''t remove non-present items: {1}." return Ganymede.createErrorDialog(this.getGSession(), "InvidDBField.deleteElements()", ts.l("deleteElements.not_found", getName(), VectorUtils.vectorString(notPresent))); } // see if our container wants to intercede in the removing operation eObj = (DBEditObject) this.owner; if (!noWizards && !local && getGSession().enableOversight) { // Wizard check retVal = eObj.wizardHook(this, DBEditObject.DELELEMENTS, invidsToDelete, null); // if a wizard intercedes, we are going to let it take the ball. if (ReturnVal.wizardHandled(retVal)) { return retVal; } } // ok, we're going to handle it. Checkpoint // so we can easily undo any changes that we make // if we have to return failure. checkkey = RandomUtils.getSaltedString("delElements[" + getName() + ":" + this.owner.getLabel() + "]"); eObj.getDBSession().checkpoint(checkkey); // may block if another thread has checkpointed this transaction try { if (!getFieldDef().isEditInPlace()) { for (Invid remote: invidsToDelete) { retVal = ReturnVal.merge(retVal, unbind(remote, local)); if (!ReturnVal.didSucceed(retVal)) { return retVal; } } // We call finalizeDeleteElements() as our last optional // step, as we are meant to guarantee performance if // finalizeDeleteElements() returns a positive result. retVal = ReturnVal.merge(retVal, eObj.finalizeDeleteElements(this, invidsToDelete)); if (ReturnVal.didSucceed(retVal)) { for (Invid remote: invidsToDelete) { currentValues.remove(remote); } this.qr = null; success = true; } return retVal; } else { // We're edit in place. We unlink an embedded object // by deleting it, and letting the asymmetric link // breaking logic in the server remove the deleted // invids out of values for us by way of // DBEditObject.finalizeRemove() and // attemptAsymBackLinkClear(). for (Invid remote: invidsToDelete) { retVal = ReturnVal.merge(retVal, eObj.getDBSession().deleteDBObject(remote)); if (!ReturnVal.didSucceed(retVal)) { return retVal; } } retVal = ReturnVal.merge(retVal, eObj.finalizeDeleteElements(this, invidsToDelete)); if (!ReturnVal.didSucceed(retVal)) { return retVal; } this.qr = null; success = true; return retVal; } } finally { if (success) { eObj.getDBSession().popCheckpoint(checkkey); } else { eObj.getDBSession().rollback(checkkey); } } } // **** // // invid_field methods // // **** /** * Returns true if this invid field may only point to objects * of a particular type. * * @see arlut.csd.ganymede.rmi.invid_field */ public boolean limited() { return getFieldDef().isTargetRestricted(); } /** * Returns a QueryResult encoded list of the current values * stored in this field. * * @see arlut.csd.ganymede.rmi.invid_field */ public synchronized QueryResult encodedValues() { QueryResult results = new QueryResult(); String label; GanymedeSession gsession = null; /* -- */ if (!isVector()) { throw new IllegalArgumentException(ts.l("global.oops_scalar", getName(), this.owner.getLabel())); } Vector<Invid> values = (Vector<Invid>) getVectVal(); gsession = getGSession(); if (gsession == null) { gsession = Ganymede.internalSession; } for (Invid invid: values) { if (gsession != null) { DBObject object = gsession.getDBSession().viewDBObject(invid); if (object == null) { Ganymede.debug(ts.l("encodedValues.bad_invid", this.owner.getLabel(), getName(), invid)); label = invid.toString(); } else { // use lookupLabel because our owner may wish to construct a custom // label for the object.. different objects may display the name // of a referenced object differently. if (this.owner instanceof DBEditObject) { label = this.owner.lookupLabel(object); } else { label = this.owner.getHook().lookupLabel(object); } } } else { label = invid.toString(); } if (label != null) { results.addRow(invid, label, false); // we're not going to report the values as editable here } } return results; } /** * This method returns true if this invid field should not * show any choices that are currently selected in field * x, where x is another field in this db_object. */ public boolean excludeSelected(db_field x) { return ((DBEditObject) this.owner).excludeSelected(x, this); } /** * Returns true if the only valid values for this invid field are in * the QueryRersult returned by choices(). In particular, if * mustChoose() returns true, <none> is not an acceptable * choice for this field after the field's value is initially set. * * @see arlut.csd.ganymede.rmi.invid_field */ public boolean mustChoose() { if (this.owner instanceof DBEditObject) { return ((DBEditObject) this.owner).mustChoose(this); } else { throw new IllegalArgumentException(ts.l("global.non_editable")); } } /** * Returns a list of acceptable invid values for this field. * * @see arlut.csd.ganymede.rmi.invid_field */ public QueryResult choices() throws NotLoggedInException { return choices(true); // by default, the filters are on } /** * <p>Returns a possibly filtered list of acceptable invid values * for this field.</p> * * @param applyFilter If true, the results returned will be filtered * by the GanymedeSession's owner filter query. * * @see arlut.csd.ganymede.rmi.invid_field */ public QueryResult choices(boolean applyFilter) throws NotLoggedInException { DBEditObject eObj; /* -- */ if (!isEditable(true)) { throw new IllegalArgumentException(ts.l("global.non_editable")); } // if choices is called, the client has asked to get // a new copy of the choice list for this field. assume // that the client's asking because it was told to ask // via a rescan command in a ReturnVal from the server, // so we need to clear the qr cache eObj = (DBEditObject) this.owner; this.qr = eObj.obtainChoiceList(this); // non-filtered if (applyFilter) { this.qr = getGSession().filterQueryResult(this.qr); } return qr; } /** * <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>If choicesKey() returns null, the client should not attempt to * use any cached values for the choice list, and should go ahead * and call choices() to get the freshly generated list.</p> */ public Object choicesKey() { if (this.owner instanceof DBEditObject) { Object key = ((DBEditObject) this.owner).obtainChoicesKey(this); // we have to be careful not to let the client try to use // its cache if our choices() method will return items that // they would normally not be able to access if (key != null) { if (((DBEditObject) this.owner).choiceListHasExceptions(this)) { return null; } } return key; } else { return null; } } // **** // // Overridable methods for implementing intelligent behavior // // **** @Override public boolean verifyTypeMatch(Object o) { return ((o == null) || (o instanceof Invid)); } /** * <p>Overridable method to verify that an object submitted to this * field has an appropriate value.</p> * * <p>This method is intended to make the final go/no go decision about * whether a given value is appropriate to be placed in this field, * by whatever means (vector add, vector replacement, scalar * replacement).</p> * * <p>This method is expected to call the * {@link arlut.csd.ganymede.server.DBEditObject#verifyNewValue(arlut.csd.ganymede.server.DBField,java.lang.Object)} * method on {@link arlut.csd.ganymede.server.DBEditObject} in order to allow custom * plugin classes to deny any given value that the plugin might not * care for, for whatever reason. Otherwise, the go/no-go decision * will be made based on the checks performed by * {@link arlut.csd.ganymede.server.DBField#verifyBasicConstraints(java.lang.Object) verifyBasicConstraints}.</p> * * <p>The ReturnVal that is returned may have transformedValue set, in * which case the code that calls this verifyNewValue() method * should consider transformedValue as replacing the 'o' parameter * as the value that verifyNewValue wants to be put into this field. * This usage of transformedValue is for canonicalizing input data.</p> * * @return A ReturnVal indicating success or failure. May * be simply 'null' to indicate success if no feedback need * be provided. */ @Override public ReturnVal verifyNewValue(Object o) { return verifyNewValue(o, false); } public ReturnVal verifyNewValue(Object o, boolean local) { DBEditObject eObj; Invid inv; /* -- */ if (debug) { System.err.print("InvidDBField.verifyNewValue("); if (o instanceof Invid) { System.err.print(Ganymede.internalSession.getDBSession().getObjectLabel((Invid) o)); } else { System.err.print(o); } System.err.println(")"); } if (!isEditable(true)) { return Ganymede.createErrorDialog(this.getGSession(), "InvidDBField.verifyNewValue()", ts.l("global.no_perms", getName(), this.owner.getLabel())); } eObj = (DBEditObject) this.owner; if (!verifyTypeMatch(o)) { return Ganymede.createErrorDialog(this.getGSession(), "InvidDBField.verifyNewValue()", ts.l("verifyNewValue.bad_type", o, getName(), this.owner.getLabel())); } inv = (Invid) o; if (inv != null) { if (limited() && (getTargetBase() != -2) && (inv.getType() != getTargetBase())) { // the invid points to an object of the wrong type return Ganymede.createErrorDialog(this.getGSession(), "InvidDBField.verifyNewValue()", ts.l("verifyNewValue.bad_object_type", inv, getName(), this.owner.getLabel(), Integer.toString(getTargetBase()))); } if (!local && mustChoose()) { if (this.qr == null && eObj.getDBSession().isInteractive()) { try { this.qr = eObj.obtainChoiceList(this); // allow any value, even if filtered } catch (NotLoggedInException ex) { return Ganymede.createErrorDialog(this.getGSession(), "InvidDBField.verifyNewValue()", ts.l("global.not_logged_in")); } } if (this.qr != null) { if (debug) { Ganymede.debug("InvidDBField.verifyNewValue(): searching for matching choice"); } if (!this.qr.containsInvid(inv)) { String invLabel = Ganymede.internalSession.getDBSession().getObjectLabel(inv); if (invLabel == null) { invLabel = inv.toString(); } if (debug) { System.err.println("InvidDBField.verifyNewValue(" + invLabel + "): didn't match against"); System.err.println(this.qr); } return Ganymede.createErrorDialog(this.getGSession(), "InvidDBField.verifyNewValue()", ts.l("verifyNewValue.bad_choice", invLabel, getName(), this.owner.getLabel())); } } } } // have our parent make the final ok on the value return eObj.verifyNewValue(this, o); } private void unlinkTarget(Invid target) { // we need to remove the asymmetric back link pointer if we're // the last field in our object to contain an asymmetric link // to target Vector<DBField> ownerFields = (Vector<DBField>) this.owner.getFieldVector(false); boolean lastLink = true; for (DBField siblingField: ownerFields) { if (siblingField == this) { continue; } if (!siblingField.getFieldDef().isInvid() || siblingField.getFieldDef().isSymmetric()) { continue; } if (siblingField.isVector()) { if (siblingField.containsElementLocal(target)) { lastLink = false; break; } } else { if (target.equals(siblingField.value)) { lastLink = false; break; } } } if (lastLink) { Ganymede.db.aSymLinkTracker.unlinkObject(getDBSession(), target, this.owner.getInvid()); } } }