/* xmlobject.java This class is a data holding structure that is intended to hold object and field data for an XML object element for xmlclient. Created: 2 May 2000 Module By: Jonathan Abbey ----------------------------------------------------------------------- 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.rmi.RemoteException; import java.util.HashMap; import java.util.Map; import java.util.Vector; import org.xml.sax.SAXException; import arlut.csd.Util.TranslationService; import arlut.csd.Util.XMLElement; import arlut.csd.Util.XMLEndDocument; import arlut.csd.Util.XMLItem; import arlut.csd.ganymede.common.FieldTemplate; import arlut.csd.ganymede.common.Invid; import arlut.csd.ganymede.common.NotLoggedInException; import arlut.csd.ganymede.common.ReturnVal; import arlut.csd.ganymede.rmi.Session; /*------------------------------------------------------------------------------ class xmlobject ------------------------------------------------------------------------------*/ /** * <p>This class is a data holding structure that is intended to hold * object and field data for an XML object element for {@link * arlut.csd.ganymede.server.GanymedeXMLSession * GanymedeXMLSession}.</p> * * @author Jonathan Abbey */ public final class xmlobject { final static boolean debug = false; /** * TranslationService object for handling string localization in the Ganymede * server. */ static TranslationService ts = TranslationService.getTranslationService("arlut.csd.ganymede.server.xmlobject"); /** * The local identifier string for this object */ String id = null; /** * Descriptive typeString for this object. This is the * contents of the <object>'s type attribute, in * XML (underscores for spaces) encoding. */ String typeString = null; /** * Action mode for this object, should be null, * "create", "edit", "delete", or "inactivate". */ String actionMode = null; /** * <p>The short object type id for this object type.</p> * * <p>Will be null if undefined.</p> */ Short type = null; /** * The server-side object identifier for this object. Will * be null until we create or locate this object in the server. */ Invid invid = null; /** * The prospective invid to create this object as. Will only be set * if the object in question has an oid attribute, and if the server * has the -magic_import flag set. */ private Invid oidCreateInvid = null; /** * If true, the invid for this field is known to not exist on the * server. */ boolean knownNonExistent = false; /** * <p>The object number, if known. This may be used to identify * an object on the server if the object is not thought to have * a unique identifier string.</p> * * <p>Will be negative one if undefined.</p> */ int num = -1; /** * Map from non-XML-coded {@link arlut.csd.ganymede.server.xmlfield * xmlfield} names to xmlfield objects. */ Map<String, xmlfield> fields = null; /** * Reference to server-side object, if we have already created it/got a reference * to it from the server. */ DBObject objref = null; /** * Create only flag. If this flag is true, this object was explicitly specified * as a new object to be created, rather than one that should be created only * if an object with the same type/id pair isn't found on the server. */ boolean forceCreate = false; /** * <p>If we're an embedded object, the owner object will refer to the * invid xmlfield that contains this embedded object.</p> * * <p>Otherwise, it will be null.</p> */ xmlfield ownerField = null; /** * <p>If we are a pre-existing embedded object, we may be identified by * an index attribute rather than an id, which the xml author may * not have easy access to.</p> * * <p>Otherwise, it will be -1.</p> */ int index = -1; /** * Reference to the GanymedeXMLSession working with us. */ GanymedeXMLSession xSession; /* -- */ /** * <p>This constructor takes the XMLElement defining an object to * be created or manipulated on the server and loads all information * for this object into the xmlobject created.</p> * * <p>This constructor reads all elements from the xmlclient * XML stream up to and including the matching close element for * this object.</p> */ public xmlobject(XMLElement openElement, GanymedeXMLSession xSession, xmlfield ownerField) throws SAXException { this.xSession = xSession; this.ownerField = ownerField; // handle any attributes in the element actionMode = openElement.getAttrStr("action"); if (actionMode != null) { actionMode = actionMode.intern(); } typeString = openElement.getAttrStr("type"); if (typeString != null) { typeString = typeString.intern(); } try { type = Short.valueOf(xSession.getTypeNum(typeString)); } catch (NullPointerException ex) { // "\n\nERROR: Unrecognized object type "{0}"" xSession.tell(ts.l("init.unrecognized_type", openElement.getAttrStr("type"))); } id = openElement.getAttrStr("id"); // may be null if (id != null) { id = id.intern(); } Integer numInt = openElement.getAttrInt("num"); if (numInt != null) { num = numInt.intValue(); } Integer indexInt = openElement.getAttrInt("index"); if (ownerField != null && indexInt != null) { index = indexInt.intValue(); } // If the server was run with the -magic_import flag, we'll record // the contents of the oid attribute as the oidCreateInvid. If it // turns out that we wind up trying to create this object, we'll // try to force the oidCreateInvid as the invid for the newly // created object. // This is used for dumping the full state of a Ganymede server // and loading it into another. We *will not* use the oid // attribute to select pre-existing objects to edit on the loading // server. if (Ganymede.allowMagicImport) { String oidString = openElement.getAttrStr("oid"); if (oidString != null) { oidCreateInvid = Invid.createInvid(oidString); } } // if we get an inactivate or delete request, our object element // might be empty, in which case, deal. if (openElement.isEmpty()) { return; } // if we're deleting or inactivating an object, we can't handle // any subelements.. so complain if we are in those modes if ("delete".equals(actionMode) || "inactivate".equals(actionMode)) { // "XMLObject error: can''t {0} a non-empty <object> element." throw new NullPointerException(ts.l("init.cant_operate", actionMode)); } // okay, we should contain some fields, then fields = new HashMap<String, xmlfield>(); XMLItem nextItem = xSession.getNextItem(); while (!nextItem.matchesClose("object") && !(nextItem instanceof XMLEndDocument)) { if (nextItem instanceof XMLElement) { // the xmlfield constructor will consume all elements up // to and including the matching field close element xmlfield field = new xmlfield(this, (XMLElement) nextItem); // xSession.tell("Added new field: " + field.toString()); fields.put(field.getName(), field); } else { // "Unrecognized XML content in object {0}: {1}" xSession.tell(ts.l("init.unrecognized_xml", openElement, nextItem)); } nextItem = xSession.getNextItem(); } if (nextItem instanceof XMLEndDocument) { // "Ran into end of XML file while parsing data object {0}" throw new RuntimeException(ts.l("init.early_end", this.toString())); } } public String getMode() { return actionMode; } /** * <p>This method causes this object to be created on * the server.</p> * * <p>This method uses the standard {@link arlut.csd.ganymede.common.ReturnVal ReturnVal} * return semantics.</p> */ public ReturnVal createOnServer(GanymedeSession session) throws NotLoggedInException { ReturnVal result; /* -- */ result = session.create_db_object(getType(), false, oidCreateInvid); if (!ReturnVal.didSucceed(result)) { return result; } objref = (DBObject) result.getObject(); Invid myInvid = objref.getInvid(); setInvid(myInvid); xSession.rememberSeenInvid(myInvid); return null; } /** * <p>This method causes this object to be checked out for editing * on the server.</p> * * <p>This method uses the standard {@link arlut.csd.ganymede.common.ReturnVal ReturnVal} * return semantics.</p> */ public ReturnVal editOnServer(GanymedeSession session) throws NotLoggedInException { ReturnVal result; Invid localInvid; /* -- */ // just to check our logic.. we shouldn't be getting a create and // an edit directive on the same object from the XML file if (objref != null) { // "xmlobject editOnServer(): Encountered duplicate xmlobject for creating or editing: {0}" return Ganymede.createErrorDialog(ts.l("editOnServer.duplicate", this.toString())); } localInvid = getInvid(); if (localInvid != null) { result = session.edit_db_object(localInvid); if (ReturnVal.didSucceed(result)) { objref = (DBObject) result.getObject(); // We'll check for duplicates here. This check is subtly // different from the one above, and its purpose is to make // sure that the GanymedeXMLSession storeObject() wasn't // fooled by a case sensitivity issue. Different case labels // may or may not map to the same Invid in the DBStore, // depending on the configuration of the object's label // handling. if (xSession.haveSeenInvid(localInvid)) { // "xmlobject editOnServer(): Encountered duplicate xmlobject for creating or editing: {0}" return Ganymede.createErrorDialog(ts.l("editOnServer.duplicate", objref.toString())); } xSession.rememberSeenInvid(localInvid); } return result; } else { throw new RuntimeException("Couldn't find object on server to edit it: " + this.toString()); } } /** * <p>This method uploads field information contained in this object * up to the Ganymede server. Unfortunately, we can't necessarily * upload all the field information all at once, as we have to * create all the objects and set enough information into them that * they can properly be addressed, before we can set all the invid * fields. The mode paramater controls this, allowing this method * to be called in multiple passes.</p> * * @param mode 0 to register all non-invids, 1 to register just * invids, 2 to register both */ public ReturnVal registerFields(int mode) throws NotLoggedInException { ReturnVal result = null; /* -- */ if (mode < 0 || mode > 2) { throw new IllegalArgumentException("mode must be 0, 1, or 2."); } if (debug) { xSession.tell("Registering fields [" + mode + "] for object " + this.toString(false)); } // we want to create/register the fields in their display order.. // this is to cohere with the expectations of custom server-side // code, which may need to have higher fields set before accepting // choices for lower fields Vector<FieldTemplate> templateVector = Ganymede.db.getObjectBase(type).getFieldTemplateVector(); for (FieldTemplate template: templateVector) { xmlfield field = fields.get(template.getName()); if (field == null) { // missing field, no big deal. just skip it. continue; } // on mode 0, we register everything but invid's (embedded // objects do not count as invids for this purpose). on mode // 1, we only register invid's. on mode 2, we register // everything. if (field.fieldDef.isInvid() && !field.fieldDef.isEditInPlace() && mode == 0) { // skip invid's continue; } else if ((!field.fieldDef.isInvid() || field.fieldDef.isEditInPlace()) && mode == 1) { // skip non-invid's continue; } result = field.registerOnServer(); if (!ReturnVal.didSucceed(result)) { return result; } } return null; } public short getType() { return type.shortValue(); } /** * <p>This method returns an invid for this xmlobject record, * performing a lookup on the server if necessary.</p> * * <p>The first time getInvid() is called, we'll try to find the * Invid from the DBStore by doing a look-up of the xml object's * label (if we're not given a num attribute). getInvid() stores * the Invid upon first lookup as a side effect.</p> */ public Invid getInvid() throws NotLoggedInException { if (invid != null || knownNonExistent) { return invid; } /* We haven't established an Invid for this object yet. Let's figure it out. */ if (num != -1) { // if we were given a number, assume they really do // mean for us to edit a pre-existing object with // that number, and don't argue invid = Invid.createInvid(type.shortValue(), num); } else if (id != null) { // try to look it up on the server if (debug) { xSession.tell("xmlobject.getInvid() calling findLabeledObject() on " + type.shortValue() + ":" + id + "[3]"); } invid = xSession.session.findLabeledObject(id, type.shortValue()); if (invid == null) { if (debug) { xSession.tell("xmlobject.getInvid() deciding known non existent on " + type + ":" + id); } knownNonExistent = true; } if (debug) { xSession.tell("xmlobject called findLabeledObject() on " + type.shortValue() + ":" + id + "[3]"); xSession.tell("findLabeledObject() returned " + invid + "[3]"); } } else if (ownerField != null && index != -1) { // if we've got no id but we do have an index, we must be an // embedded object that already existed on the server. Let's // find our containing field and parent object and get the // invid for the embedded object with matching index. xmlobject ownerObject = ownerField.owner; Invid parentInvid = ownerObject.getInvid(); try { ReturnVal parentResult = xSession.session.view_db_object(parentInvid); DBObject parentObject = (DBObject) parentResult.getObject(); InvidDBField containerField = parentObject.getInvidField(ownerField.fieldDef.getID()); invid = (Invid) containerField.getElementLocal(index); } catch (RemoteException ex) { } catch (NullPointerException ex) { } } return invid; } /** * This method sets the invid for this object, if it is discovered * from the server during processing. Used to provide invids for * newly created embedded objects, for instance. */ public void setInvid(Invid invid) { this.invid = invid; this.knownNonExistent = false; } /** * This method returns a field definition for a named field. * The fieldName string is assumed to be underscore-for-space XML * encoded. */ public FieldTemplate getFieldDef(String fieldName) { return xSession.getFieldTemplate(type, fieldName); } public String toString() { return this.toString(false); } public String toString(boolean showAll) { StringBuilder result = new StringBuilder(); result.append("<object type=\""); result.append(typeString); result.append("\""); if (id != null) { result.append(" id=\""); result.append(id); result.append("\""); } if (num != -1) { result.append(" num=\""); result.append(num); result.append("\""); } result.append(">"); if (showAll) { result.append("\n"); // add the fields in the server's display order Vector<FieldTemplate> templateVector = xSession.getTemplateVector(type); for (FieldTemplate template: templateVector) { xmlfield field = (xmlfield) fields.get(template.getName()); if (field == null) { // missing field, no big deal. just skip it. continue; } result.append("\t" + field + "\n"); } result.append("</object>"); } return result.toString(); } }