/* GASH 2 GanymedeXMLSession.java The GANYMEDE object storage system. Created: 1 August 2000 Module By: Jonathan Abbey, jonabbey@arlut.utexas.edu ----------------------------------------------------------------------- Ganymede Directory Management System Copyright (C) 1996-2014 The University of Texas at Austin Ganymede is a registered trademark of The University of Texas at Austin Contact information Web site: http://www.arlut.utexas.edu/gash2 Author Email: ganymede_author@arlut.utexas.edu Email mailing list: ganymede@arlut.utexas.edu US Mail: Computer Science Division Applied Research Laboratories The University of Texas at Austin PO Box 8029, Austin TX 78713-8029 Telephone: (512) 835-3200 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package arlut.csd.ganymede.server; import java.io.IOException; import java.io.PipedOutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.rmi.RemoteException; import java.rmi.server.Unreferenced; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Vector; import org.xml.sax.SAXException; import arlut.csd.JDialog.JDialogBuff; import arlut.csd.Util.booleanSemaphore; import arlut.csd.Util.StringUtils; import arlut.csd.Util.TranslationService; import arlut.csd.Util.VectorUtils; import arlut.csd.Util.XMLCloseElement; import arlut.csd.Util.XMLElement; import arlut.csd.Util.XMLEndDocument; import arlut.csd.Util.XMLError; import arlut.csd.Util.XMLItem; import arlut.csd.Util.XMLStartDocument; import arlut.csd.Util.XMLUtils; import arlut.csd.Util.XMLWarning; import arlut.csd.ganymede.common.FieldTemplate; import arlut.csd.ganymede.common.FieldType; import arlut.csd.ganymede.common.Invid; import arlut.csd.ganymede.common.NotLoggedInException; import arlut.csd.ganymede.common.ReturnVal; import arlut.csd.ganymede.rmi.Base; import arlut.csd.ganymede.rmi.NameSpace; import arlut.csd.ganymede.rmi.Session; import arlut.csd.ganymede.rmi.XMLSession; /*------------------------------------------------------------------------------ class GanymedeXMLSession ------------------------------------------------------------------------------*/ /** * <p>This class handles all XML loading operations for the Ganymede * server. GanymedeXMLSession's are created by the {@link * arlut.csd.ganymede.rmi.Server Server}'s {@link * arlut.csd.ganymede.rmi.Server#xmlLogin(java.lang.String username, * java.lang.String password) xmlLogin()} method. A * GanymedeXMLSession is created on top of a {@link * arlut.csd.ganymede.server.GanymedeSession GanymedeSession} and * interacts with the database through that session. A * GanymedeXMLSession generally looks to the rest of the server like * any other client, except that if the XML file contains a * <ganyschema> section, the GanymedeXMLSession will attempt to * manipulate the server's login semaphore to force the server into * schema editing mode. This will fail if there are any remote * clients connected to the server at the time the XML file is * processed.</p> * * <p>Once xmlLogin creates (and RMI exports) a GanymedeXMLSession, an * xmlclient repeatedly calls the {@link * arlut.csd.ganymede.server.GanymedeXMLSession#xmlSubmit(byte[]) * xmlSubmit()} method, which writes the bytes received into a pipe. * The GanymedeXMLSession's thread (also initiated by * GanymedeServer.xmlLogin()) then loops, reading data off of the pipe * with an {@link arlut.csd.Util.XMLReader XMLReader} and doing * various schema editing and data loading operations.</p> * * <p>The <ganydata> processing section was originally written * as part of xmlclient, and did all xml parsing on the client side * and all data operations remotely over RMI. Pulling this logic into * a server-side GanymedeXMLSession sped things up by a factor of 300 * in my testing.</p> */ public final class GanymedeXMLSession extends java.lang.Thread implements XMLSession, Unreferenced { public static final boolean debug = false; public static final boolean schemadebug = true; /** * How big shall we make our default invid/xmlobject hash size when * we're processing objects in an object base? This should be small * enough not to be too great a waste, but big enough that we won't * have to worry about lots of hashtable re-hashing during * transaction processing. */ private static final int OBJECTHASHSIZE = 10001; /** * TranslationService object for handling string localization in * the Ganymede server. */ static final TranslationService ts = TranslationService.getTranslationService("arlut.csd.ganymede.server.GanymedeXMLSession"); /** * This major version number is compared with the "major" * attribute in the Ganymede XML document element. We won't * try to read Ganymede XML files whose major and/or minor numbers * are too high. */ public static final int majorVersion = 1; /** * This minor version number is compared with the "minor" * attribute in the Ganymede XML document element. We won't * try to read Ganymede XML files whose major and/or minor numbers * are too high. */ public static final int minorVersion = 1; /** * The working GanymedeSession underlying this XML session. */ GanymedeSession session; /** * The XML parser object handling XML data from the client */ arlut.csd.Util.XMLReader reader; /** * The data stream used to write data from the client to the * XML parser. */ private PipedOutputStream pipe; /** * The default buffer size in the {@link arlut.csd.Util.XMLReader XMLReader}. * This value determines how far ahead the XMLReader's i/o thread can get in * reading from the XML file. Higher or lower values of this variable may * give better performance, depending on the characteristics of the JVM with * regards threading, etc. */ private int bufferSize = 100; /** * Map from names to Maps mapping field names to {@link * arlut.csd.ganymede.common.FieldTemplate FieldTemplate} objects. */ private Map<String, Map<String, FieldTemplate>> objectTypes = new HashMap<String, Map<String, FieldTemplate>>(); /** * Map from Short object type ids to Maps mapping field names to * {@link arlut.csd.ganymede.common.FieldTemplate FieldTemplate} * objects. */ private Map<Short, Map<String, FieldTemplate>> objectTypeIDs = new HashMap<Short, Map<String, FieldTemplate>>(); /** * <p>Rather overloaded HashMap mapping Short type ids to Maps from * local object designations (either id Strings or num Integers from * the <object> elements) either to actual {@link * arlut.csd.ganymede.server.xmlobject xmlobject} records or to raw * {@link arlut.csd.ganymede.common.Invid Invids}.</p> * * <p>The purpose of this structure is to efficiently (time-wise) track * targets for the <invid> elements that are encountered * during processing of an XML transaction stream. In many cases, * these targets will be <object> elements that have not yet * been created in the server's persistent data store. Not all, * however. In the cases where they properly refer to pre-existing * objects on the server that are not edited by <object> * elements in the XML transaction, the inner hashtable structures * will contain simple Invid objects rather than xmlobjects.</p> */ private Map<Short, Map<Object,Object>> objectStore = new HashMap<Short, Map<Object,Object>>(); /** * HashSet used to detect <object> elements that map to the same Invid * in the DBStore. */ private HashSet<Invid> duplications = null; /** * Vector of {@link arlut.csd.ganymede.server.xmlobject xmlobjects} * that correspond to new Ganymede server objects * that have been/need to be created by this GanymedeXMLSession. */ private Vector<xmlobject> createdObjects = new Vector<xmlobject>(); /** * Vector of {@link arlut.csd.ganymede.server.xmlobject xmlobjects} * that correspond to pre-existing Ganymede * server objects that have been/need to be checked out for editing by this * GanymedeXMLSession. */ private Vector<xmlobject> editedObjects = new Vector<xmlobject>(); /** * Vector of {@link arlut.csd.ganymede.server.xmlobject * xmlobjects} that correspond to Ganymede server objects that have * been created/checked out for editing during embedded invid field * processing, and which need to have their invid fields registered * after everything else is done. */ private Vector<xmlobject> embeddedObjects = new Vector<xmlobject>(); /** * Vector of {@link arlut.csd.ganymede.server.xmlobject xmlobjects} * that correspond to pre-existing Ganymede * server objects that have been/need to be inactivated by this * GanymedeXMLSession. */ private Vector<xmlobject> inactivatedObjects = new Vector<xmlobject>(); /** * Vector of {@link arlut.csd.ganymede.server.xmlobject xmlobjects} * that correspond to pre-existing Ganymede * server objects that have been/need to be deleted by this * GanymedeXMLSession. */ private Vector<xmlobject> deletedObjects = new Vector<xmlobject>(); /** * This StringWriter holds output generated by the GanymedeXMLSession's * parser thread. */ private StringWriter errBuf = new StringWriter(); /** * This PrintWriter is used to handle all debug/error output * on behalf of the GanymedeXMLSession. */ private PrintWriter err = new PrintWriter(this.errBuf); /** * <p>This flag is used to track whether the background parser thread * is active.</p> * * <p>We set it true here so that we avoid any race conditions.</p> */ private booleanSemaphore parsing = new booleanSemaphore(true); /** * This flag is used to track whether the background parser thread * was successful in committing the transaction. */ private boolean success = false; /** * If we are editing the server's schema from the XML source, this * field will hold a reference to a DBSchemaEdit object. */ private DBSchemaEdit editor = null; /** * This vector is used by the XML schema editing logic to track * namespaces from the xml file that need to be added to the current * schema. Elements in this vector are empty XMLElements that contain * name and optional case-sensitive attributes. */ private Vector<XMLItem> spacesToAdd; /** * This vector is used by the XML schema editing logic to track * namespaces from the xml file that need to be removed from the * current schema. Elements in this vector are Strings representing * the level of name spaces to be deleted.. */ private Vector<String> spacesToRemove; /** * This vector is used by the XML schema editing logic to track * namespaces from the xml file that need to be edited in the * current schema. Since namespaces can only be edited in the sense * of toggling the case sensitivity flag, this vector will only * contain XMLElements for namespaces that need to have their case * sensitivity toggled. Elements in this vector are empty * XMLElements that contain name and optional case-sensitive * attributes. */ private Vector<XMLItem> spacesToEdit; /** * This vector is used by the XML schema editing logic to track * object types from the xml file that need to be added to the current * schema. Elements in this vector are XMLItem trees rooted * with the appropriate <objectdef> elements. */ private Vector<XMLItem> basesToAdd; /** * This vector is used by the XML schema editing logic to track * object types in the current schema that were not mentioned in the * xml file and thus need to be removed from the current * schema. Elements of this vector are the names of existing bases * to be removed. */ private Vector<String> basesToRemove; /** * This vector is used by the XML schema editing logic to track * object types from the xml file that need to be edited in the * current schema. Elements in this vector are XMLItem trees rooted * with the appropriate <objectdef> elements. */ private Vector<XMLItem> basesToEdit; /** * This XMLItem is the XMLElement root of the namespace tree, * rooted with the <namespaces> element. Children of this * node will be <namespace> elements. */ private XMLItem namespaceTree = null; /** * This XMLItem is the XMLElement root of the category tree, * rooted with the top-level <category> element. * Children of this node will be either <category> or * <objectdef> elements. */ private XMLItem categoryTree = null; /** * Comment for the <ganydata> transaction commit, if any is * provided. */ private String comment = null; /** * Semaphore to gate the cleanup() method. */ private booleanSemaphore cleanedup = new booleanSemaphore(false); /** * We'll cache our identity so that we can keep identifying our * logging with our identity even after our underlying * GanymedeSession is cleared out. */ private String identity = null; /* -- */ public GanymedeXMLSession(GanymedeSession session) { this.session = session; // tell the GanymedeSession about us, so they can notify us with // the stopParser() method if our server login gets forcibly // revoked. this.session.setXSession(this); this.identity = this.session.getSessionName(); try { // We create a PipedOutputStream that we will write data from // the XML client into. The XMLReader will create a matching // PipedInputStream internally, that it will use to read data // that we feed into the pipe. // // Used only for processing input from xmlclient. If the // xmlclient only wanted to dump data, it would just use a // GanymedeSession and call one of that class' getXML() // methods this.pipe = new PipedOutputStream(); this.reader = new arlut.csd.Util.XMLReader(this.pipe, bufferSize, true, this.err); } catch (IOException ex) { errPrintln(ts.l("init.initialization_error", Ganymede.stackTrace(ex))); throw new RuntimeException(ex.getMessage()); } } /** * This method returns a remote reference to the underlying * GanymedeSession in use on the server. * * @see arlut.csd.ganymede.rmi.XMLSession */ public Session getSession() { return this.session; } /** * This method is called repeatedly by the XML client in order to * send the next packet of XML data to the server. If the server * has detected any errors in the already-received XML stream, * xmlSubmit() may return a non-null ReturnVal with a description of * the failure. Otherwise, the xmlSubmit() method will enqueue the * XML data for the server's continued processing and immediately * return a null value, indicating success. The xmlSubmit() method * will only block if the server has filled up its internal buffers * and must wait to digest more of the already submitted XML. * * @see arlut.csd.ganymede.rmi.XMLSession */ public ReturnVal xmlSubmit(byte[] bytes) throws NotLoggedInException { this.session.checklogin(); if (debug) { errPrintln("xmlSubmit called on server"); } if (this.parsing.isSet()) { try { if (debug) { errPrintln("xmlSubmit byting"); } pipe.write(bytes); // can block if the parser thread gets behind if (debug) { errPrintln("xmlSubmit bit"); } } catch (IOException ex) { // the XMLReader may provide our error buffer with more // details about what happened after the parser closes the // pipe, so we'll spin here for a bit until the reader // finishes with everything and closes down. // but, because we're not nuts, we'll not wait more than // 10 seconds // note also that we don't assume that reader is not null here.. if // the parser throws an exception, it's possible that that won't // directly cause our run() method to terminate with an exception, // so we'll have to try and do cleanup ourselves.. if the run() // method does do an exception, we may have already cleaned up // before we get called, so we check to make sure that reader is // not null int waitCount = 0; while (this.reader != null && !this.reader.isDone() && waitCount < 40) { // "Waiting for reader to close down: {0,number,#}" errPrintln(ts.l("xmlSubmit.waiting_for_reader", Integer.valueOf(waitCount))); try { Thread.sleep(250); // sleep for a quarter second } catch (InterruptedException ex2) { // ? } waitCount++; } cleanup(); try { return getReturnVal(false); } finally { if (debug) { errPrintln("xmlSubmit call returned on server 1"); } } } } else { // "GanymedeXMLSession.xmlSubmit(), parser already closed, skipping writing into pipe." errPrintln(ts.l("xmlSubmit.parser_already_closed")); } // if reader is not done, we're ok to continue try { return getReturnVal(this.reader != null && !this.reader.isDone()); } finally { if (debug) { errPrintln("xmlSubmit call returned on server 2"); } } } /** * <p>This method is called by the XML client once the end of the * XML stream has been transmitted, whereupon the server will * attempt to finalize the XML transaction and return an overall * success or failure indication in the ReturnVal.</p> * * <p>xmlEnd() only returns a success / failure indication in the * returned ReturnVal. In order to get all diagnostic / progress * messages explaining the success or failure, the client is obliged * to maintain a thread calling getNextErrChunk() until * getNextErrChunk() returns null.</p> * * @see arlut.csd.ganymede.rmi.XMLSession */ public ReturnVal xmlEnd() { if (debug) { errPrintln("xmlEnd() called"); } this.parsing.waitForCleared(); return getReturnVal(this.success); } /** * <p>Returns chunks of diagnostic / progress messages produced on * the server during the processing of XML submitted with * xmlSubmit().</p> * * <p>This call will block on the server until more message data is * available and will for at least a tenth of a second while the XML * is still being processed so that the client doesn't loop on * getNextErrChunk() too fast.</p> * * <p>Once this XMLSession has finished processing the submitted XML * and everything in the diagnostic / progress message stream has * been retrieved by calls to getNextErrChunk(), getNextErrChunk() * will return null.</p> * * <p>The XML client is meant to run a dedicated thread to * repeatedly call this method to collect diagnostic / progress data * until getNextErrChunk() returns null. This thread will generally * last beyond the time of the XML client's xmlEnd() call.</p> * * @see arlut.csd.ganymede.rmi.XMLSession */ public String getNextErrChunk() { StringBuffer errBuffer = this.errBuf.getBuffer(); String progress; /* -- */ // block for output or until xml processing completes while (errBuffer.length() == 0 && this.parsing.waitForCleared(50)); // then delay up to a hundred milliseconds to accumulate more // output and delay the remote client from spinning too fast this.parsing.waitForCleared(100); synchronized (errBuffer) { progress = errBuffer.toString(); errBuffer.setLength(0); } if (progress.length() == 0) { return null; } else { errPrintln(progress); return progress; } } /** * Writes to server stderr with an identifying prefix */ private void errPrintln(String x) { // StringUtils.insertPrefixPerLine will make sure we end with a // newline, we don't need to do System.err.println() here. System.err.print(StringUtils.insertPrefixPerLine(x, getLogPrefix())); } /** * Writes to server stderr */ private void errPrint(String x) { System.err.print(x); } /** * Returns an identifying prefix string that should be prepended to * logging to the Ganymede stderr.. */ private String getLogPrefix() { return "xml [" + this.identity + "]: "; } /** * This method is for use on the server, and is called by the * GanymedeSession to let us know if the server is forcing our login * off. * * @see arlut.csd.ganymede.rmi.XMLSession */ public void abort() { if (debug) { errPrintln("GanymedeXMLSession abort"); try { throw new RuntimeException("GanymedeXMLSession abort trace"); } catch (RuntimeException ex) { Ganymede.logError(ex); } } // if the parser thread has completed, then parsing will be false // and the XML reader will have already been closed if (this.parsing.isSet()) { if (debug) { errPrintln("GanymedeXMLSession closing reader"); } // "Abort called, closing reader." errPrintln(ts.l("abort.aborting")); this.reader.close(); // this will cause the XML Reader to halt } else { if (debug) { errPrintln("GanymedeXMLSession closing already closed reader"); } } } /** * <p>This method is called when the Java RMI system detects that this * remote object is no longer referenced by any remote objects.</p> * * <p>This method handles abnormal logouts and time outs for us. By * default, the 1.1 RMI time-out is 10 minutes.</p> * * <p>The RMI timeout can be modified by setting the system property * sun.rmi.transport.proxy.connectTimeout.</p> * * @see java.rmi.server.Unreferenced */ public void unreferenced() { if (this.session != null) { // set our underlying GanymedeSession's xSession to null so // that it will take things seriously when we tell it that it // is unreferenced. this.session.setXSession(null); this.session.unreferenced(); } } /** * This method handles cleanup post-schema edit. */ public void cleanupSchemaEdit() { if (this.spacesToAdd != null) { this.spacesToAdd.setSize(0); this.spacesToAdd = null; } if (this.spacesToRemove != null) { this.spacesToRemove.setSize(0); this.spacesToRemove = null; } if (this.spacesToEdit != null) { this.spacesToEdit.setSize(0); this.spacesToEdit = null; } if (this.basesToAdd != null) { this.basesToAdd.setSize(0); this.basesToAdd = null; } if (this.basesToRemove != null) { this.basesToRemove.setSize(0); this.basesToRemove = null; } if (this.basesToEdit != null) { this.basesToEdit.setSize(0); this.basesToEdit = null; } if (this.namespaceTree != null) { this.namespaceTree.dissolve(); this.namespaceTree = null; } if (this.categoryTree != null) { this.categoryTree.dissolve(); this.categoryTree = null; } } /** * Something to assist in garbage collection. */ public void cleanup() { if (debug) { errPrintln("Entering cleanup"); } if (this.cleanedup.set(true)) { return; } // note, we must not clear errBuf here, as the client will keep // calling getNextErrChunk() until it has received the entire // output generated before this.parsing is set to false. // // likewise we're not going to null out our parsing semaphore. this.reader.close(); this.reader = null; this.objectTypes.clear(); this.objectTypes = null; this.objectTypeIDs.clear(); this.objectTypeIDs = null; this.objectStore.clear(); this.objectStore = null; this.createdObjects.setSize(0); this.createdObjects = null; this.editedObjects.setSize(0); this.editedObjects = null; this.embeddedObjects.setSize(0); this.embeddedObjects = null; this.inactivatedObjects.setSize(0); this.inactivatedObjects = null; this.deletedObjects.setSize(0); this.deletedObjects = null; if (this.session != null && this.session.isLoggedIn()) { this.session.logout(); this.session = null; } } /** * This method handles the actual XML processing in the * background. All activity which ultimately draws from * the XMLReader will block as necessary to wait for more * data from the client. */ public synchronized void run() { try { if (debug) { errPrintln("GanymedeXMLSession run getting startDocument"); } XMLItem startDocument = getNextItem(); if (!(startDocument instanceof XMLStartDocument)) { // "XML parser error: first element {0} not an XMLStartDocument" tell(ts.l("run.not_start_element", startDocument)); return; } if (debug) { errPrintln("GanymedeXMLSession run getting docElement"); } XMLItem docElement = getNextItem(); if (!docElement.matches("ganymede")) { // "Error, XML Stream does not contain a Ganymede XML file.\nUnrecognized XML element: {0}" tell(ts.l("run.bad_start_element", docElement)); return; } Integer majorI = docElement.getAttrInt("major"); Integer minorI = docElement.getAttrInt("minor"); if (majorI == null || majorI.intValue() > majorVersion) { // "Error, the Ganymede document element {0} does not contain a compatible major version number." tell(ts.l("run.bad_major_version", docElement)); return; } if (majorI.intValue() == majorVersion && (minorI == null || minorI.intValue() > minorVersion)) { // "Error, the Ganymede document element {0} does not contain a compatible minor version number." tell(ts.l("run.bad_minor_version", docElement)); return; } // okay, we're good to go XMLItem nextElement = getNextItem(); if (nextElement.matches("ganyschema")) { boolean schemaOk = false; try { schemaOk = processSchema(nextElement); } finally { cleanupSchemaEdit(); } if (!schemaOk) { return; } else { // so far, so good. if we don't proceed to find a // ganydata element, we'll want to return a positive // success result to the client's xmlEnd() call. this.success = true; } nextElement = getNextItem(); } if (nextElement.matches("ganydata")) { this.success = processData(); if (!this.success) { // don't bother processing rest of XML doc.. just jump // down to finally clause return; } nextElement = getNextItem(); } while (!nextElement.matchesClose("ganymede") && !(nextElement instanceof XMLEndDocument)) { if (!(nextElement instanceof XMLCloseElement)) { // "Skipping unrecognized element: {0}" tell(ts.l("run.skipping", nextElement)); } nextElement = getNextItem(); } } catch (Exception ex) { // we may get a SAXException here if the reader gets // shutdown before our parsing process is done, or if // there is something malformed in the XML // "Caught exception for GanymedeXMLSession.run():\n{0}" tell(ts.l("run.exception", Ganymede.stackTrace(ex))); } finally { if (debug) { errPrintln("run() terminating"); } this.err.close(); this.parsing.set(false); cleanupSchemaEdit(); cleanup(); } } /** * Helper method to process events from the {@link * arlut.csd.Util.XMLReader XMLReader}. By using this method, the * rest of the code in GanymedeXMLSession doesn't have to check for * error and warning conditions. */ public XMLItem getNextItem() throws SAXException { XMLItem item = null; item = this.reader.getNextItem(); if (item instanceof XMLError) { throw new SAXException(item.toString()); } while (item instanceof XMLWarning) { // "Warning!: {0}" tell(ts.l("getNextItem.warning", item)); item = this.reader.getNextItem(); } return item; } /** * Helper method to peek at the next event from the {@link * arlut.csd.Util.XMLReader XMLReader}. If the peek finds an * XMLError item, a SAXException will be thrown. If the peek finds * any XMLWarning items, they will be consumed and the contents of * the warning text passed to err. */ public XMLItem peekNextItem() throws SAXException { XMLItem item = null; item = this.reader.peekNextItem(); if (item instanceof XMLError) { throw new SAXException(item.toString()); } while (item instanceof XMLWarning) { // "Warning!: {0}" tell(ts.l("getNextItem.warning", item)); this.reader.getNextItem(); // consume the peeked warning item item = this.reader.peekNextItem(); } return item; } /** * <p>This method is called after the <ganyschema> element has * been read and consumes everything up to and including the * matching </ganyschema> element, if such is to be found.</p> * * <p>Assuming a valid <ganyschema> tree is read, this method * will perform the actual edits to the server's schema required to * bring the server's schema definition into compliance with that * specified by the incoming XML stream.</p> */ public boolean processSchema(XMLItem ganySchemaItem) throws SAXException { boolean _success = false; XMLItem _schemaTree = this.reader.getNextTree(ganySchemaItem); try { // okay, from this point forward, we're going to assume // failure unless/until we get to the end of the editing // process. The finally clause for this try block will use // the success variable to decide whether to commit or abort // the schema edit. // the getNextTree() method will have either succeeded or // failed in its entirety.. if it found an error along the // way, it will have just returned that information, so check // that before we get crazy and start messing with the schema if ((_schemaTree instanceof XMLError) || (_schemaTree instanceof XMLEndDocument)) { tell(_schemaTree.toString()); return false; } if (!this.session.getPermManager().isSuperGash()) { // "Skipping <ganyschema> element.. not logged in with supergash privileges." tell(ts.l("processSchema.bad_permissions")); return false; } // getNextTree will throw back an XMLError or XMLEndDocument if // such is encountered while scanning in the tree's subitems // try to get a schema editing context this.editor = editSchema(); if (this.editor == null) { // "Couldn''t edit the schema.. other users logged in?" tell(ts.l("processSchema.editing_blocked")); return false; } // do the thing XMLItem _schemaChildren[] = _schemaTree.getChildren(); if (_schemaChildren == null) { _success = true; // no editing to be done return true; } // if schemaChildren was not null, XMLReader will guarantee // that it has at least one element int _nextchild = 0; if (_schemaChildren[_nextchild].matches("namespaces")) { this.namespaceTree = _schemaChildren[_nextchild++]; } if (_schemaChildren.length > _nextchild && _schemaChildren[_nextchild].matches("object_type_definitions")) { XMLItem _otdItem = _schemaChildren[_nextchild]; if (_otdItem.getChildren() == null || _otdItem.getChildren().length != 1) { // "Error, the object_type_definitions element does not contain a single-rooted category tree." tell(ts.l("processSchema.bad_category_tree")); return false; } this.categoryTree = _otdItem.getChildren()[0]; } else { // "Couldn''t find <object_type_definitions>." tell(ts.l("processSchema.no_object_type_definitions")); return false; } // 1. calculate what name spaces need to be created, edited, or removed if (schemadebug) { // "1. Calculate what name spaces need to be created, edited, or removed" tell(ts.l("processSchema.schemadebug_1")); } if (this.namespaceTree != null) { if (!calculateNameSpaces()) { return false; } } // calculateNameSpaces() filled in spacesToAdd, spacesToRemove, and spacesToEdit // 2. create new name spaces if (schemadebug) { // "2. Create new name spaces." tell(ts.l("processSchema.schemadebug_2")); } for (XMLItem _space: this.spacesToAdd) { String _name = _space.getAttrStr("name"); if (_name == null || _name.equals("")) { // "Error, namespace item {0} has no name attribute." tell(ts.l("processSchema.no_name_namespace", _space)); return false; } // make sure we have a case-sensitive attribute, just to // get in the user's face a bit so he doesn't have the // system doing something unexpected without warning if (_space.getAttrStr("case-sensitive") == null) { // "Warning, namespace item {0} has no case-sensitive attribute. {0} will be created as case insensitive." tell(ts.l("processSchema.no_case_namespace", _space)); } boolean _sensitive = _space.getAttrBoolean("case-sensitive"); // "\tCreating namespace {0}." tell(ts.l("processSchema.creating_namespace", _name)); NameSpace _aNewSpace = this.editor.createNewNameSpace(_name,!_sensitive); if (_aNewSpace == null) { // "Couldn''t create a new namespace for item {0}." tell(ts.l("processSchema.failed_namespace_create", _space)); return false; } } // 3. calculate what bases we need to create, edit, or remove if (schemadebug) { // "3. Calculate what object bases we need to create, edit, or remove." tell(ts.l("processSchema.schemadebug_3")); } if (categoryTree == null || !calculateBases()) { return false; } // calculateBases filled in basesToAdd, basesToRemove, and basesToEdit. // 4. delete any bases that are not at least mentioned in the XML schema tree if (schemadebug) { // "4. Delete any object bases that are not at least mentioned in the XML schema tree." tell(ts.l("processSchema.schemadebug_4")); } for (String _basename: this.basesToRemove) { // "\tDeleting object base {0}." tell(ts.l("processSchema.deleting_base", _basename)); if (!handleReturnVal(this.editor.deleteBase(_basename))) { return false; } } // 5. rename any bases that need to be renamed if (schemadebug) { // "5. Rename any object bases that need to be renamed." tell(ts.l("processSchema.schemadebug_5")); } if (!handleBaseRenaming()) { return false; } // 6. create all bases on the basesToAdd list if (schemadebug) { // "6. Create all object bases on the basesToAdd list." tell(ts.l("processSchema.schemadebug_6")); } for (XMLItem _entry: this.basesToAdd) { // "\tCreating object base {0}" tell(ts.l("processSchema.creating_objectbase", _entry.getAttrStr("name"))); Integer _id = _entry.getAttrInt("id"); boolean _embedded = false; XMLItem _children[] = _entry.getChildren(); if (_children != null) { for (XMLItem _child: _children) { if (_child.matches("embedded")) { _embedded = true; break; } } } // create the new base, with the requested id. we'll // specify that the object base is not an embedded one, // since DBObjectBase.setXML() can change that if need be. // also, we'll put it in the root category just so we can // get things in the category tree before we resequence it DBObjectBase _newBase = this.editor.createNewBase(this.editor.getRootCategory(), _embedded, _id.shortValue()); // if we failed to create the base, we'll have an // exception thrown.. our finally clause and higher level // catches will handle it // don't yet try to resolve invid links, since we haven't // done a pass through basesToEdit to fix up fields yet if (!handleReturnVal(_newBase.setXML(_entry, false, this.err))) { return false; } } // 7. fix up fields in pre-existing bases if (schemadebug) { // "7. Fix up fields in pre-existing object bases." tell(ts.l("processSchema.schemadebug_7")); } for (XMLItem _entry: this.basesToEdit) { Integer _id = _entry.getAttrInt("id"); DBObjectBase _oldBase = this.editor.getBase(_id.shortValue()); if (_oldBase == null) { // " Error, couldn''t find DBObjectBase for {0} in pass {1,number,#}." tell(ts.l("processSchema.bad_base", _entry.getTreeString(), Integer.valueOf(1))); return false; } if (false) { // "7. pass 1 - fixups on {0}" tell(ts.l("processSchema.schemadebug_7_1", _oldBase.getName())); } // "\tEditing object base {0}" tell(ts.l("processSchema.editing_objectbase", _oldBase.getName())); // don't yet try to resolve invid links, since we haven't // done a complete pass through basesToEdit to fix up // fields yet if (!handleReturnVal(_oldBase.setXML(_entry, false, this.err))) { return false; } } // now that we have completed our first pass through fields in // basesToAdd and basesToEdit, where we created and/or renamed // fields, so now we can go back through both lists and finish // fixing up invid links. for (XMLItem _entry: this.basesToAdd) { Integer _id = _entry.getAttrInt("id"); DBObjectBase _oldBase = this.editor.getBase(_id.shortValue()); if (_oldBase == null) { // " Error, couldn''t find DBObjectBase for {0} in pass {1,number,#}." tell(ts.l("processSchema.bad_base", _entry.getTreeString(), Integer.valueOf(2))); return false; } if (schemadebug) { // "7. pass 2 - fixups on object base {0}" tell(ts.l("processSchema.schemadebug_7_2", _oldBase.getName())); } // tell("\tResolving " + _oldBase); if (!handleReturnVal(_oldBase.setXML(_entry, true, this.err))) { return false; } } for (XMLItem _entry: this.basesToEdit) { Integer _id = _entry.getAttrInt("id"); DBObjectBase _oldBase = this.editor.getBase(_id.shortValue()); if (_oldBase == null) { // " Error, couldn''t find DBObjectBase for {0} in pass {1,number,#}." tell(ts.l("processSchema.bad_base", _entry.getTreeString(), Integer.valueOf(3))); return false; } if (schemadebug) { // "7. pass 3 - fixups on object base {0}" tell(ts.l("processSchema.schemadebug_7_3", _oldBase.getName())); } if (!handleReturnVal(_oldBase.setXML(_entry, true, this.err))) { return false; } } // 8. Shuffle the category tree to match the XML file if (schemadebug) { // "8. Shuffle the Category tree to match the XML schema." tell(ts.l("processSchema.schemadebug_8")); } if (!handleReturnVal(reshuffleCategories(categoryTree))) { return false; } // 9. Clear out any namespaces that need it if (schemadebug) { // "9. Clear out any name spaces that need it." tell(ts.l("processSchema.schemadebug_9")); } for (String _name: spacesToRemove) { // "\tDeleting name space {0}." tell(ts.l("processSchema.deleting_namespace", _name)); if (!handleReturnVal(this.editor.deleteNameSpace(_name))) { return false; } } // 10. Need to flip case sensitivity on namespaces that // need it if (schemadebug) { // "10. Need to flip case sensitivity on namespaces that need it." tell(ts.l("processSchema.schemadebug_10")); } for (XMLItem _entry: spacesToEdit) { String _name = _entry.getAttrStr("name"); boolean _val = _entry.getAttrBoolean("case-sensitive"); // "\tFlipping name space {0}." tell(ts.l("processSchema.flipping_namespace", _name)); NameSpace _space = this.editor.getNameSpace(_name); _space.setInsensitive(!_val); } // 11. Woohoo, Martha, I is a-coming home! if (schemadebug) { // "Successfully completed XML schema edit." tell(ts.l("processSchema.schemadebug_success")); } _success = true; } catch (Throwable ex) { // "Caught Exception during XML schema editing.\n{0}" tell(ts.l("processSchema.exception", Ganymede.stackTrace(ex))); _success = false; return false; } finally { // break apart the XML item tree for gc ganySchemaItem.dissolve(); _schemaTree.dissolve(); // either of these will clear the semaphore lock // created by editSchema() above if (_success) { // "Committing schema edit." tell(ts.l("processSchema.committing")); this.editor.commit(); this.editor = null; return true; } else { // "Releasing schema edit." tell(ts.l("processSchema.releasing")); if (this.editor != null) { this.editor.release(); this.editor = null; } return false; } } } /** * This method fills in spacesToAdd, spacesToRemove, and spacesToEdit. */ private boolean calculateNameSpaces() { try { NameSpace[] _list = this.editor.getNameSpaces(); Vector<String> _current = new Vector<String>(_list.length); for (NameSpace _ns: _list) { // theoretically possible RemoteException here, due to remote interface _current.add(_ns.getName()); } XMLItem _XNamespaces[] = this.namespaceTree.getChildren(); Vector<String> _newSpaces = new Vector<String>(_XNamespaces.length); Map<String, XMLItem> _entries = new HashMap<String, XMLItem>(_XNamespaces.length); for (XMLItem _xns: _XNamespaces) { if (!_xns.matches("namespace")) { // "Error, unrecognized element: {0} when expecting <namespace>." tell(ts.l("calculateNameSpaces.not_a_namespace", _xns)); return false; } String _name = _xns.getAttrStr("name"); // ditto remote if (_entries.containsKey(_name)) { // "Error, found duplicate <namespace> name ''{0}''." tell(ts.l("calculateNameSpaces.duplicate_namespace", _name)); return false; } _entries.put(_name, _xns); _newSpaces.add(_name); } // for spacesToRemove, we just keep the names for the missing // name spaces spacesToRemove = VectorUtils.difference(_current, _newSpaces); // for spacesToAdd and spacesToEdit, we need to first identify // names that are new or that were already in our current // namespaces list, then look up and save the appropriate // XMLItem nodes in the spacesToAdd and spacesToEdit global // Vectors. Vector<String> _additions = VectorUtils.difference(_newSpaces, _current); this.spacesToAdd = new Vector<XMLItem>(); for (String _name: _additions) { this.spacesToAdd.add(_entries.get(_name)); } Vector<String> _possibleEdits = VectorUtils.intersection(_newSpaces, _current); spacesToEdit = new Vector<XMLItem>(); // we are only interested in namespaces to be edited if the // case-sensitivity changes. we could defer this check, but // since we know that case-sensitivity is the only thing that // can vary in a namespace other than its name, we'll go ahead // and filter out no-changes here. for (String _name: _possibleEdits) { XMLItem _entry = _entries.get(_name); NameSpace _oldEntry = this.editor.getNameSpace(_name); // yes, ==, not !=.. note that the _oldEntry check is for // insensitivity, not sensitivity. if (_entry.getAttrBoolean("case-sensitive") == _oldEntry.isCaseInsensitive()) { spacesToEdit.add(_entry); } } } catch (RemoteException ex) { Ganymede.logError(ex); throw new RuntimeException(ex.getMessage()); } return true; } /** * This method fills in basesToAdd, basesToRemove, and basesToEdit. */ private boolean calculateBases() { // create a list of Short base id's for of bases that we have // registered in the schema at present DBObjectBase[] list = this.editor.getDBBases(); Vector<Short> current = new Vector(list.length); for (DBObjectBase base: list) { current.add(base.getKey()); } // get a list of objectdef root nodes from our xml tree Vector<XMLItem> newBases = new Vector<XMLItem>(); findBasesInXMLTree(categoryTree, newBases); // get a list of Short id's from our xml tree, record // a mapping from those id's to the objectdef nodes in // our xml tree Vector<Short> xmlBases = new Vector<Short>(); Map<Short, XMLItem> entries = new HashMap<Short, XMLItem>(); // for Short id's HashSet<String> nameTable = new HashSet<String>(); // for checking for redundant names for (XMLItem objectdef: newBases) { Integer id = objectdef.getAttrInt("id"); String name = XMLUtils.XMLDecode(objectdef.getAttrStr("name")); if (id == null) { // "Error, couldn''t get id number for object definition item: {0}." tell(ts.l("calculateBases.missing_id", objectdef)); return false; } if (id.shortValue() < 0) { // "Error, can''t create or edit an object base with a negative id number: {0}." tell(ts.l("calculateBases.negative_id", objectdef)); return false; } if (name == null || name.equals("")) { // "Error, couldn''t get name for object definition item: {0}." tell(ts.l("calculateBases.missing_name", objectdef)); return false; } Short key = Short.valueOf(id.shortValue()); xmlBases.add(key); if (entries.containsKey(key)) { // "Error, found duplicate object base id number in <ganyschema>: {0}." tell(ts.l("calculateBases.duplicate_id", objectdef)); return false; } if (nameTable.contains(name)) { // "Error, found duplicate object base name in <ganyschema>: {0}." tell(ts.l("calculateBases.duplicate_name", objectdef)); return false; } entries.put(key, objectdef); nameTable.add(name); } // We need to calculate basesToRemove.. since the DBSchemaEditor // can only delete bases based on their names, we need to // take the Vector of Shorts that we get from difference and // put the matching names into basesToRemove Vector<Short> deletions = VectorUtils.difference(current, xmlBases); this.basesToRemove = new Vector<String>(); for (Short id: deletions) { this.basesToRemove.add(this.editor.getBase(id.shortValue()).getName()); } // now calculate basesToAdd and basesToEdit, recording the // objectdef XMLItem root for each base in each list Vector<Short> additions = VectorUtils.difference(xmlBases, current); Vector<Short> edits = VectorUtils.intersection(xmlBases, current); this.basesToAdd = new Vector<XMLItem>(); for (Short id: additions) { XMLItem entry = entries.get(id); if (entry.getAttrInt("id").shortValue() < 256) { // "Error, object type ids of less than 256 are reserved for new system-defined // object types, and may not be created with the xml schema editing system: {0}." tell(ts.l("calculateBases.reserved_object_base_id", entry)); return false; } this.basesToAdd.add(entry); } this.basesToEdit = new Vector<XMLItem>(); for (Short id: edits) { XMLItem entry = entries.get(id); this.basesToEdit.add(entry); } return true; } /** * This is a recursive method to do a traversal of an XMLItem * tree, adding object base definition roots found to the foundBases * vector. */ private void findBasesInXMLTree(XMLItem treeRoot, Vector<XMLItem> foundBases) { // objectdef's will contain fielddef children, but no more // objectdef's, so we treat objectdef's as leaf nodes for our // traversal if (treeRoot.matches("objectdef")) { foundBases.add(treeRoot); return; } XMLItem children[] = treeRoot.getChildren(); if (children == null) { return; } for (XMLItem childRoot: children) { findBasesInXMLTree(childRoot, foundBases); } } /** * This private method takes care of doing any object type * renaming, prior to resolving invid field definitions. */ private boolean handleBaseRenaming() throws RemoteException { Base numBaseRef; Base nameBaseRef; String name; /* -- */ for (XMLItem myBaseItem: this.basesToEdit) { name = XMLUtils.XMLDecode(myBaseItem.getAttrStr("name")); numBaseRef = this.editor.getBase(myBaseItem.getAttrInt("id").shortValue()); if (name.equals(numBaseRef.getName())) { continue; // no rename necessary } // we need to rename the base pointed to by numBaseRef.. first // see if another base already has the name we want nameBaseRef = this.editor.getBase(name); // if we found a base with the name we need, switch the two // names. we know from calculateBases() that the user // didn't put two bases by the same name in the xml <ganyschema> // section, so if swap the names, we'll fix up the second name // when we get to it // "\tRenaming {0} to {1}." tell(ts.l("handleBaseRenaming.renaming_base", numBaseRef.getName(), name)); if (nameBaseRef != null) { String swapName = numBaseRef.getName(); if (!handleReturnVal(numBaseRef.setName(name))) { return false; } if (!handleReturnVal(nameBaseRef.setName(swapName))) { return false; } } else { if (!handleReturnVal(numBaseRef.setName(name))) { return false; } } } return true; } /** * This method is used by the XML schema editing code * in {@link arlut.csd.ganymede.server.GanymedeXMLSession GanymedeXMLSession} * to fix up the category tree to match that specified in the XML * <ganyschema> element. */ public synchronized ReturnVal reshuffleCategories(XMLItem categoryRoot) { HashSet<String> categoryNames = new HashSet<String>(); if (!testXMLCategories(categoryRoot, categoryNames)) { // "Error, category names not unique in XML schema." return Ganymede.createErrorDialog(ts.l("reshuffleCategories.duplicate_category")); } DBBaseCategory _rootCategory = buildXMLCategories(categoryRoot); if (_rootCategory == null) { // "Error, buildXMLCategories() was not able to create a new category tree." return Ganymede.createErrorDialog(ts.l("reshuffleCategories.failed_categories")); } this.editor.rootCategory = _rootCategory; return null; // tada! } /** * This method tests an XML category tree to make sure that all * categories in the tree have unique names. */ public boolean testXMLCategories(XMLItem categoryRoot, HashSet<String> names) { if (categoryRoot.matches("category")) { // make sure we don't get duplicate category names if (names.contains(categoryRoot.getAttrStr("name"))) { return false; } else { names.add(categoryRoot.getAttrStr("name")); } XMLItem children[] = categoryRoot.getChildren(); if (children == null) { return true; } for (XMLItem childRoot: children) { if (!testXMLCategories(childRoot, names)) { return false; } } } return true; } /** * This recursive method takes an XMLItem category tree and returns * a new DBBaseCategory tree with all categories and object definitions * from the XMLItem category tree ordered correctly. */ public DBBaseCategory buildXMLCategories(XMLItem categoryRoot) { DBBaseCategory _root; /* -- */ if (!categoryRoot.matches("category")) { // "buildXMLCategories() called with a bad XML element. Expecting <category> element, found {0}." tell(ts.l("buildXMLCategories.bad_root", categoryRoot)); return null; } try { _root = new DBBaseCategory(Ganymede.db, categoryRoot.getAttrStr("name")); } catch (RemoteException ex) { // "Caught RMI export error in buildXMLCategories():\n{0}" tell(ts.l("buildXMLCategories.exception", Ganymede.stackTrace(ex))); return null; } XMLItem _children[] = categoryRoot.getChildren(); if (_children == null) { return _root; } for (XMLItem _child: _children) { if (_child.matches("category")) { _root.addNodeAfter(buildXMLCategories(_child), null); } else if (_child.matches("objectdef")) { DBObjectBase _base = this.editor.getBase(_child.getAttrInt("id").shortValue()); _root.addNodeAfter(_base, null); } } return _root; } /** * <p>This method is called after the <ganydata> element has * been read and consumes everything up to and including the * matching </ganydata> element, if such is to be found.</p> * * <p>Before starting to read data from the <ganydata> * element, this method communicates with the Ganymede server * database through the normal client {@link * arlut.csd.ganymede.rmi.Session Session} interface.</p> * * <p>The contents of <ganydata> are scanned, and an in-memory * datastructure is constructed in the GanymedeXMLSession. All * objects are organized in memory by type and id, and inter-object * invid references are resolved to the extent possible.</p> * * <p>If all of that succeeds, processData() will start a * transaction on the server, and will start transferring the data * from the XML file's <ganydata> element into the database. * If any errors are reported, the returned error message is printed * and processData aborts. If no errors are reported at this stage, * a transaction commit is attempted. Once again, if there are any * errors reported from the server, they are printed and processData * aborts. Otherwise, success!</p> * * @return true if the <ganydata> element was successfully * processed, or false if a fatal error in the XML stream was * encountered during processing */ public boolean processData() throws SAXException { XMLItem item = null; boolean committedTransaction = false; int modCount = 0; int totalCount = 0; /* -- */ if (debug) { tell("processData"); } initializeLookups(); try { item = getNextItem(); while (!item.matchesClose("ganydata") && !(item instanceof XMLEndDocument)) { if (item.matches("comment") && this.reader.isNextCharData()) { this.comment = this.reader.getFollowingString(item, true); } else if (item.matches("object")) { xmlobject objectRecord = null; try { objectRecord = new xmlobject((XMLElement) item, this, null); } catch (NullPointerException ex) { // if we have already cleaned up as a result of the parser // throwing a pipe write exception, don't report this // exception, as it ultimately came from another thread if (cleanedup.isSet()) { return false; } // otherwise, it was probably due to something in the xmlobject // constructor, and we should report it.. // bad field or object error.. return out of this // method without committing the transaction // our finally clause will log us out // "Error constructing xmlobject for {0}:\n{1}" tell(ts.l("processData.xmlobject_init_failure", item, Ganymede.stackTrace(ex))); return false; } if (modCount == 9) { errPrint("."); modCount = 0; } else { modCount++; } totalCount++; String mode = objectRecord.getMode(); if (mode == null || mode.equals("create")) { // if no mode was specified, we'll tentatively // identify it as an object that needs to be // created.. but when it comes time to look at // that, we'll look up the object identifier // attributes, and if we find a pre-existing // match, we'll edit that instead. // if they did specify "create" as the object // action mode, this object definition record will // be forced into a new object, rather than trying // to look for an object on the server with // matching identity attributes // this can be useful if the user wants to create // new objects without worrying about whether // there are id conflicts with the server's state if (mode != null) { objectRecord.forceCreate = true; } this.createdObjects.add(objectRecord); } else if (mode.equals("edit")) { this.editedObjects.add(objectRecord); } else if (mode.equals("delete")) { this.deletedObjects.add(objectRecord); } else if (mode.equals("inactivate")) { this.inactivatedObjects.add(objectRecord); } if (!storeObject(objectRecord)) { tell(""); // "Error, xml object {0} is not uniquely identified within the XML file." tell(ts.l("processData.duplicate_xmlobject", objectRecord)); // our finally clause will log us out return false; } } item = getNextItem(); } tell(""); // "Done scanning XML for data elements. Integrating transaction for {0,number,#} <object> elements." tell(ts.l("processData.integrating", Integer.valueOf(totalCount))); tell(""); try { this.duplications = new HashSet<Invid>(); committedTransaction = integrateXMLTransaction(); } finally { this.duplications = null; } if (committedTransaction) { // "Finished integrating XML data transaction." tell(ts.l("processData.committed")); } return committedTransaction; } catch (Exception ex) { // "Error, processData() caught an exception:\n{0}" tell(ts.l("processData.exception", Ganymede.stackTrace(ex))); return false; } finally { this.reader.pushbackItem(item); // let the run() method see what we ran into at the end if (!committedTransaction) { // "Aborted XML data transaction, logging out." tell(ts.l("processData.aborted")); } this.session.logout(); } } /** * This private method handles data structures initialization for * the GanymedeXMLSession, prepping hash lookups that are used * to accelerate XML processing. */ private void initializeLookups() { if (debug) { errPrintln("GanymedeXMLSession: initializeLookups"); } for (DBObjectBase base: Ganymede.db.getBases()) { Vector<FieldTemplate> templates = base.getFieldTemplateVector(); Map<String, FieldTemplate> fieldHash = new HashMap<String, FieldTemplate>(); for (FieldTemplate tmpl: templates) { fieldHash.put(tmpl.getName(), tmpl); } this.objectTypes.put(base.getName(), fieldHash); this.objectTypeIDs.put(Short.valueOf(base.getTypeID()), fieldHash); } } /** * <p>This method records an xmlobject that has been loaded from the * XML file into the GanymedeXMLSession objectStore hash.</p> * * <p>This method returns false if the object to be stored has an id * conflict with a previously stored object.</p> */ public boolean storeObject(xmlobject object) { if (false) { errPrintln("GanymedeXMLSession: storeObject(" + object + ")"); } Map<Object,Object> objectHash = this.objectStore.get(object.type); if (objectHash == null) { objectHash = new HashMap<Object,Object>(OBJECTHASHSIZE, 0.75f); this.objectStore.put(object.type, objectHash); } if (object.id != null) { if (objectHash.containsKey(object.id)) { Object thing = objectHash.get(object.id); if (thing instanceof xmlobject) { // we've already got an xmlobject with that id stored return false; } else if (thing instanceof Invid) { // we've got a previously cached Invid associated with // this object's id.. go ahead and replace it with an // actual xmlobject. Invid objectInvid; try { objectInvid = object.getInvid(); } catch (NotLoggedInException ex) { throw new RuntimeException(ex); // really can't happen } if (!thing.equals(objectInvid)) { if (objectInvid == null) { object.setInvid((Invid) thing); } else { // ugh! we seem to be storing an xmlobject // that thinks it belongs to an Invid that // doesn't match a previous one associated // with this slot. that can't possibly be // right, can it? return false; } } objectHash.put(object.id, object); } else { throw new ClassCastException(); } } else { objectHash.put(object.id, object); } } else if (object.num != -1) { Integer intKey = Integer.valueOf(object.num); if (objectHash.containsKey(intKey)) { Object thing = objectHash.get(intKey); if (thing instanceof xmlobject) { return false; } else if (thing instanceof Invid) { // overwrite the cached Invid. Note that since the // object being stored has its Invid forced with the // use of the num field, there's no way that this // xmlobject we're storing can't match the Invid // already stored in this slot. objectHash.put(intKey, object); } else { throw new ClassCastException(); } } else { objectHash.put(intKey, object); } } return true; } /** * This method is used to look up an xmlobject that we have seen, * in order to get a partial resolution of an invid target that we * have found in our XML processing. It is called by {@link * arlut.csd.ganymede.server.xInvid#getInvid()} in the event that an * >invid< element is found which does not resolve to a * pre-existing object in the server. */ public xmlobject getXMLObjectTarget(short typeId, String objectId) { Map<Object,Object> objectHash = this.objectStore.get(Short.valueOf(typeId)); if (objectHash == null) { return null; } Object result = objectHash.get(objectId); if (result != null && result instanceof xmlobject) { return (xmlobject) result; } else { return null; } } /** * <p>This method resolves an Invid from a type/id pair, talking * to the server if the type/id pair has not previously been seen.</p> * * <p>Returns null on failure to retrieve.</p> * * @param typeId The object type number of the invid to find * @param objId The unique label of the object */ public Invid getInvid(short typeId, String objId) throws NotLoggedInException { Invid invid = null; Short typeKey; Map<Object,Object> objectHash; /* -- */ typeKey = Short.valueOf(typeId); objectHash = this.objectStore.get(typeKey); if (objectHash == null) { // we do this mainly so we can fall through to our if (element // == null) logic below. objectHash = new HashMap<Object,Object>(OBJECTHASHSIZE, 0.75f); this.objectStore.put(typeKey, objectHash); } Object element = objectHash.get(objId); if (element == null) { // okay, let's look up the given label in the database to see // if the user is trying to refer to a pre-existing object. // note that we really shouldn't be doing this before we have // looped through and done a storeObject() on all objects in // the xml <ganydata> section, or else we might prematurely // store a reference to a pre-existing object when the xml // file meant to reference an object defined in it if (false) { tell("Calling findLabeledObject() on " + typeId + ":" + objId); } invid = this.session.findLabeledObject(objId, typeId); if (debug) { tell("Returned from findLabeledObject() on " + typeId + ":" + objId); tell("findLabeledObject() returned " + invid); } if (invid != null) { // cache it in our objectStore so that we won't have to do // (relatively) expensive lookups from here on out. objectHash.put(objId, invid); } } else { if (element instanceof xmlobject) { invid = ((xmlobject) element).getInvid(); if (debug) { tell("GanymedeXMLSession.getInvid() found xmlobject in objectHash for " + typeId + ":" + objId); tell("Found xmlobject is " + element.toString()); } // if invid is null at this point, this object hasn't been // created or edited yet on the server, so we can't do // anything other than return null } else { // we'll just go ahead and throw a ClassCastException if // we've got something strange in our objectHash invid = (Invid) element; } } return invid; } /** * <p>This method resolves an Invid from a type/num pair</p> * * <p>Returns null on failure to retrieve.</p> * * @param typename The name of the object type, in XML encoded form * @param num The numeric id of */ public Invid getInvid(String typename, int num) { return Invid.createInvid(getTypeNum(typename), num); } /** * This method retrieves an xmlobject that has been previously * loaded from the XML file. * * @param baseName An XML-encoded object type string * @param objectID The id string for the object in question */ public xmlobject getObject(String baseName, String objectID) { return getObject(Short.valueOf(getTypeNum(baseName)), objectID); } /** * This method retrieves an xmlobject that has been previously * loaded from the XML file. * * @param baseID a Short holding the number of object type sought * @param objectID The id string for the object in question */ public xmlobject getObject(Short baseID, String objectID) { Map<Object,Object> objectHash = this.objectStore.get(baseID); if (objectHash == null) { return null; } Object thing = objectHash.get(objectID); if (thing != null && thing instanceof xmlobject) { return (xmlobject) thing; } return null; } /** * This method retrieves an xmlobject that has been previously * loaded from the XML file. * * @param baseName An XML-encoded object type string * @param objectNum The Integer object number for the object sought */ public xmlobject getObject(String baseName, Integer objectNum) { return getObject(Short.valueOf(getTypeNum(baseName)), objectNum); } /** * This method retrieves an xmlobject that has been previously * loaded from the XML file. * * @param baseID a Short holding the number of object type sought * @param objectNum The Integer object number for the object sought */ public xmlobject getObject(Short baseID, Integer objectNum) { Map<Object,Object> objectHash = this.objectStore.get(baseID); if (objectHash == null) { return null; } Object thing = objectHash.get(objectNum); if (thing != null && thing instanceof xmlobject) { return (xmlobject) thing; } return null; } /** * <p>This helper method returns the short id number of an object * type based on its underscore-for-space encoded XML object type * name.</p> * * <p>If the named object type cannot be found, a * NullPointerException will be thrown.</p> */ public short getTypeNum(String objectTypeName) { // this is currently using a linear search.. we probably should // try to fix this at some point, but the number of object types // in the server n is likely to be really quite low, so this // probably won't hurt too bad DBObjectBase base = Ganymede.db.getObjectBase(XMLUtils.XMLDecode(objectTypeName)); if (base == null) { throw new NullPointerException("Oh, why won't you let my people look up: " + objectTypeName + ", oh my lord?"); } return base.getTypeID(); } /** * <p>This helper method returns the object type string for an object * type based on its short object type ID number.</p> * * <p>If the named object type cannot be found, a * NullPointerException will be thrown.</p> */ public String getTypeName(short objectTypeID) { DBObjectBase base = Ganymede.db.getObjectBase(objectTypeID); return base.getName(); } /** * <p>This helper method returns a hash of field names to * {@link arlut.csd.ganymede.common.FieldTemplate FieldTemplate} based * on the underscore-for-space XML encoded object type name.</p> * * <p>The Map returned by this method is intended to be used with * the getObjectFieldType method.</p> */ public Map<String, FieldTemplate> getFieldHash(String objectTypeName) { return this.objectTypes.get(XMLUtils.XMLDecode(objectTypeName)); } /** * This helper method takes a hash of field names to * {@link arlut.csd.ganymede.common.FieldTemplate FieldTemplate} and an * underscore-for-space XML encoded field name and returns the * FieldTemplate for that field, if known. If not, null is * returned. */ public FieldTemplate getObjectFieldType(Map<String, FieldTemplate> fieldHash, String fieldName) { return fieldHash.get(XMLUtils.XMLDecode(fieldName)); } /** * This helper method takes a short object type id and an * underscore-for-space XML encoded field name and returns the * FieldTemplate for that field, if known. If not, null is * returned. */ public FieldTemplate getFieldTemplate(short type, String fieldName) { return getFieldTemplate(Short.valueOf(type), fieldName); } /** * This helper method takes a short object type id and an * underscore-for-space XML encoded field name and returns the * FieldTemplate for that field, if known. If not, null is * returned. */ public FieldTemplate getFieldTemplate(Short type, String fieldName) { Map<String, FieldTemplate> fieldHash = this.objectTypeIDs.get(type); if (fieldHash == null) { return null; } return fieldHash.get(XMLUtils.XMLDecode(fieldName)); } public Vector<FieldTemplate> getTemplateVector(Short type) { DBObjectBase base = Ganymede.db.getObjectBase(type); return base.getFieldTemplateVector(); } public Vector<FieldTemplate> getTemplateVector(short type) { DBObjectBase base = Ganymede.db.getObjectBase(type); return base.getFieldTemplateVector(); } public boolean haveSeenInvid(Invid paramInvid) { return this.duplications.contains(paramInvid); } public void rememberSeenInvid(Invid paramInvid) { this.duplications.add(paramInvid); } public void rememberEmbeddedObject(xmlobject object) { this.embeddedObjects.add(object); } /** * This method actually does the work of integrating our data into the * DBStore. * * @return true if the data was successfully integrated to the server and * the transaction committed successfully, false if the transaction * had problems and was abandoned. */ private boolean integrateXMLTransaction() throws NotLoggedInException { ReturnVal retVal; HashMap<String, Integer> editCount = new HashMap<String, Integer>(); HashMap<String, Integer> createCount = new HashMap<String, Integer>(); HashMap<String, Integer> deleteCount = new HashMap<String, Integer>(); HashMap<String, Integer> inactivateCount = new HashMap<String, Integer>(); /* -- */ if (this.cleanedup.isSet()) { return false; } retVal = this.session.openTransaction("xmlclient", false); // non-interactive if (!ReturnVal.didSucceed(retVal)) { if (retVal.getDialog() != null) { // "GanymedeXMLSession Error: couldn''t open transaction {0}: {1}" tell(ts.l("integrateXMLTransaction.failed_open_msg", this.session.getSessionName(), retVal.getDialog().getText())); } else { // "GanymedeXMLSession Error: couldn''t open transaction {0}." tell(ts.l("integrateXMLTransaction.failed_open_no_msg", this.session.getSessionName())); } return false; } this.session.enableWizards(false); // we're not interactive, don't give us no wizards // we first need to try to resolve all objects in our various // queues to find their invids. if we find ones that don't match // with objects pre-existing on the server, but do match other // <object> elements in the file, we'll provisionally link them to // the xmlobject representing the object in question. knitInvidReferences(); try { for (xmlobject newObject: this.createdObjects) { boolean newlyCreated = false; // if the object has enough information that we can look it up // on the server (and get an Invid for it), assume that it // already exists and go ahead and pull it for editing rather // than creating it, unless the forceCreate flag is on. if (newObject.forceCreate || newObject.getInvid() == null) { incCount(createCount, newObject.typeString); if (debug) { errPrintln("Creating " + newObject); } newlyCreated = true; retVal = newObject.createOnServer(this.session); if (!ReturnVal.didSucceed(retVal)) { String msg = retVal.getDialogText(); if (msg != null) { // "GanymedeXMLSession Error creating object {0}:\n{1}" throw new XMLIntegrationException(ts.l("integrateXMLTransaction.creating_error_msg", newObject, msg)); } else { // "GanymedeXMLSession Error detected creating object {0}, but no specific error message was generated." throw new XMLIntegrationException(ts.l("integrateXMLTransaction.creating_error_no_msg", newObject)); } } } else { incCount(editCount, newObject.typeString); if (debug) { errPrintln("Editing pre-existing " + newObject); } retVal = newObject.editOnServer(this.session); if (!ReturnVal.didSucceed(retVal)) { String msg = retVal.getDialogText(); if (msg != null) { // "GanymedeXMLSession Error editing object {0}:\n{1}" throw new XMLIntegrationException(ts.l("integrateXMLTransaction.editing_error_msg", newObject, msg)); } else { // "GanymedeXMLSession Error detected editing object {0}, but no specific error message was generated." throw new XMLIntegrationException(ts.l("integrateXMLTransaction.editing_error_no_msg", newObject)); } } } // we can't be sure that we can register invid fields // until all objects that we need to create are // created.. for now, just register non-invid fields retVal = newObject.registerFields(0); // everything but invids if (!ReturnVal.didSucceed(retVal)) { String msg = retVal.getDialogText(); if (msg != null) { if (newlyCreated) { // "[1] Error registering fields for newly created object {0}:\n{1}" throw new XMLIntegrationException(ts.l("integrateXMLTransaction.error_new_registering", newObject, msg)); } else { // "[1] Error registering fields for edited object {0}:\n{1}" throw new XMLIntegrationException(ts.l("integrateXMLTransaction.error_old_registering", newObject, msg)); } } else { if (newlyCreated) { // "[1] Error detected registering fields for newly created object {0}." throw new XMLIntegrationException(ts.l("integrateXMLTransaction.error_new_registering_no_msg", newObject)); } else { // "[1] Error detected registering fields for edited object {0}." throw new XMLIntegrationException(ts.l("integrateXMLTransaction.error_old_registering_no_msg", newObject)); } } } } // the created (or possibly) created objects are created and/or // edited, and their non-invid fields are fixed up. we need to do // the same for definitely edited objects for (xmlobject object: this.editedObjects) { incCount(editCount, object.typeString); retVal = object.editOnServer(this.session); if (!ReturnVal.didSucceed(retVal)) { String msg = retVal.getDialogText(); if (msg != null) { // "GanymedeXMLSession Error editing object {0}:\n{1}" throw new XMLIntegrationException(ts.l("integrateXMLTransaction.editing_error_msg", object, msg)); } else { // "GanymedeXMLSession Error detected editing object {0}, but no specific error message was generated." throw new XMLIntegrationException(ts.l("integrateXMLTransaction.editing_error_no_msg", object)); } } retVal = object.registerFields(0); // everything but non-embedded invid fields if (!ReturnVal.didSucceed(retVal)) { String msg = retVal.getDialogText(); if (msg != null) { // "[{0,number,#}] Error registering fields for {1}:\n{2}" throw new XMLIntegrationException(ts.l("integrateXMLTransaction.error_registering", Integer.valueOf(2), object, msg)); } else { // "[{0,number,#}] Error detected registering fields for {1}." throw new XMLIntegrationException(ts.l("integrateXMLTransaction.error_registering_no_msg", Integer.valueOf(2), object)); } } } // at this point, all objects we need to create are created, // and any non-invid fields in those new objects have been // registered. We now need to register any invid fields in // the newly created objects, which should be able to resolve // now. for (xmlobject newObject: this.createdObjects) { retVal = newObject.registerFields(1); // just invids if (!ReturnVal.didSucceed(retVal)) { String msg = retVal.getDialogText(); if (msg != null) { // "[{0,number,#}] Error registering fields for {1}:\n{2}" throw new XMLIntegrationException(ts.l("integrateXMLTransaction.error_registering", Integer.valueOf(3), newObject, msg)); } else { // "[{0,number,#}] Error detected registering fields for {1}." throw new XMLIntegrationException(ts.l("integrateXMLTransaction.error_registering_no_msg", Integer.valueOf(3), newObject)); } } } // now we need to register fields in the edited objects for (xmlobject object: this.editedObjects) { retVal = object.registerFields(1); // just invids, everything else we already did if (!ReturnVal.didSucceed(retVal)) { String msg = retVal.getDialogText(); if (msg != null) { // "[{0,number,#}] Error registering fields for {1}:\n{2}" throw new XMLIntegrationException(ts.l("integrateXMLTransaction.error_registering", Integer.valueOf(4), object, msg)); } else { // "[{0,number,#}] Error detected registering fields for {1}." throw new XMLIntegrationException(ts.l("integrateXMLTransaction.error_registering_no_msg", Integer.valueOf(4), object)); } } } // finally we need to do the same for the objects we checked out // or created when handling embedded objects for (xmlobject object: this.embeddedObjects) { retVal = object.registerFields(1); // only non-embedded invids if (!ReturnVal.didSucceed(retVal)) { String msg = retVal.getDialogText(); if (msg != null) { // "[{0,number,#}] Error registering fields for {1}:\n{2}" throw new XMLIntegrationException(ts.l("integrateXMLTransaction.error_registering", Integer.valueOf(5), object, msg)); } else { // "[{0,number,#}] Error detected registering fields for {1}." throw new XMLIntegrationException(ts.l("integrateXMLTransaction.error_registering_no_msg", Integer.valueOf(5), object)); } } } // now we need to inactivate any objects to be inactivated for (xmlobject object: this.inactivatedObjects) { incCount(inactivateCount, object.typeString); Invid target = object.getInvid(); if (target == null) { // "Error, couldn''t find Invid for object to be inactivated: {0}" throw new XMLIntegrationException(ts.l("integrateXMLTransaction.what_invid_to_inactivate", object)); } retVal = this.session.inactivate_db_object(target); if (!ReturnVal.didSucceed(retVal)) { String msg = retVal.getDialogText(); if (msg != null) { // "Error inactivating {0}:\n{1}" throw new XMLIntegrationException(ts.l("integrateXMLTransaction.bad_inactivation", object, msg)); } else { // "Error detected inactivating {0}." throw new XMLIntegrationException(ts.l("integrateXMLTransaction.bad_inactivation_no_msg", object)); } } } // and we need to delete any objects to be deleted for (xmlobject object: this.deletedObjects) { Invid target = object.getInvid(); if (target == null) { // "Error, couldn''t find Invid for object to be deleted: {0}" tell(ts.l("integrateXMLTransaction.what_invid_to_delete", object)); continue; } incCount(deleteCount, object.typeString); retVal = this.session.remove_db_object(target); if (!ReturnVal.didSucceed(retVal)) { String msg = retVal.getDialogText(); if (msg != null) { // "Error deleting {0}:\n{1}" throw new XMLIntegrationException(ts.l("integrateXMLTransaction.bad_deletion", object, msg)); } else { // "Error detected deleting {0}." throw new XMLIntegrationException(ts.l("integrateXMLTransaction.bad_deletion_no_msg", object)); } } } // "Committing transaction." tell(ts.l("integrateXMLTransaction.committing")); tell(""); retVal = this.session.commitTransaction(true, // abort on fail this.comment); if (!ReturnVal.didSucceed(retVal)) { String msg = retVal.getDialogText(); if (msg != null) { // "Error, could not successfully commit this XML data transaction:\n{0}" throw new XMLIntegrationException(ts.l("integrateXMLTransaction.commit_error", msg)); } else { // "Error detected committing XML data transaction." throw new XMLIntegrationException(ts.l("integrateXMLTransaction.commit_error_no_msg")); } } if (createCount.size() > 0) { // "Objects created:" tell(ts.l("integrateXMLTransaction.objects_created")); for (Map.Entry<String, Integer> item: createCount.entrySet()) { // "\t{0}: {1,number,#}" tell(ts.l("integrateXMLTransaction.object_count", item.getKey(), item.getValue())); } } if (editCount.size() > 0) { // "Objects edited:" tell(ts.l("integrateXMLTransaction.objects_edited")); for (Map.Entry<String, Integer> item: editCount.entrySet()) { // "\t{0}: {1,number,#}" tell(ts.l("integrateXMLTransaction.object_count", item.getKey(), item.getValue())); } } if (deleteCount.size() > 0) { // "Objects deleted:" tell(ts.l("integrateXMLTransaction.objects_deleted")); for (Map.Entry<String, Integer> item: deleteCount.entrySet()) { // "\t{0}: {1,number,#}" tell(ts.l("integrateXMLTransaction.object_count", item.getKey(), item.getValue())); } } if (inactivateCount.size() > 0) { // "Objects inactivated:" tell(ts.l("integrateXMLTransaction.objects_inactivated")); for (Map.Entry<String, Integer> item: inactivateCount.entrySet()) { // "\t{0}: {1,number,#}" tell(ts.l("integrateXMLTransaction.object_count", item.getKey(), item.getValue())); } } // "Transaction successfully committed." tell(ts.l("integrateXMLTransaction.thrill_of_victory")); return true; } catch (RuntimeException ex) { if (!(ex instanceof XMLIntegrationException)) { tell(ex); } else { tell(ex.getMessage()); } // "Errors encountered, aborting transaction. " tell(ts.l("integrateXMLTransaction.agony_of_defeat")); return false; } } /** * <p>This private helper method is responsible for working through * the objectStore hash and dereferencing any xInvids contained * therein to objects in the XML file and/or server, before any * actual edits are performed.</p> * * <p>This is necessary so that we can deal with the possibility of * objects being renamed before we have all of our invid field * updates made. By looking up all Invids that we can before we * start editing anything (but after we've done a storeObject() on * all objects that we are editing or creating), we can be sure that * we will resolve Invid references in xInvid objects to the proper, * pre-rename object labels.</p> */ private void knitInvidReferences() throws NotLoggedInException { List<xmlobject> xmlObjectsToProcess = new ArrayList<xmlobject>(); /* -- */ for (Map.Entry<Short, Map<Object,Object>> entry: this.objectStore.entrySet()) { Short type = entry.getKey(); Map<Object, Object> objectHash = entry.getValue(); for (Map.Entry<Object, Object> innerEntry: objectHash.entrySet()) { Object key = innerEntry.getKey(); Object thing = innerEntry.getValue(); if (thing instanceof xmlobject) { xmlobject storedObject = (xmlobject) thing; // Let's try to get the invid for this storedObject, // as it exists before we might possibly do any // renaming. The call to getInvid() on the xmlobject // may involve a lookup in the server's persistent // data store if we haven't previously resolved it. Invid invid = storedObject.getInvid(); if (invid == null) { if (storedObject.getMode() == null || storedObject.getMode().equals("create")) { // we may need to create this object, so we'll // clear the knownNonExistent flag. storedObject.knownNonExistent = false; } else { // "Error, could not look up pre-existing {0} object with label {1}. Did you mean to use the create action?" throw new RuntimeException(ts.l("knitInvidReferences.no_such_object", getTypeName(type.shortValue()), key)); } } if (storedObject.fields != null) { xmlObjectsToProcess.add(storedObject); } } } } // Now that we have forced the lookup and resolution of all // labeled objects, we need to go through all objects that we've // seen reference to and try to look up all <invid> elements // contained therein. // // <invid> elements that point to objects we // have just looked up above will be able to dereference those // Invids by looking in the objectStore hashing structure, even // for objects that we have labeled but not yet created on the // server. // // Since the xmlfield dereferenceInvids() method can alter the // objectStore hash structure, we're working with our own List of // the xmlobjects we've seen, to avoid a // ConcurrentModificationException. for (xmlobject storedObject: xmlObjectsToProcess) { for (xmlfield field: storedObject.fields.values()) { if (field.getType() == FieldType.INVID && !field.fieldDef.isEditInPlace()) { field.dereferenceInvids(); } } } } /** * this private helper method increments a counting * integer in table, keyed by type. */ private void incCount(HashMap<String, Integer> table, String type) { Integer x = table.get(type); if (x == null) { table.put(type, Integer.valueOf(1)); } else { table.put(type, Integer.valueOf(x.intValue() + 1)); } } /** * This private helper method creates a ReturnVal object to be * passed back to the xmlclient. */ private ReturnVal getReturnVal(boolean success) { if (success) { return null; // success, nothing to report } else { return new ReturnVal(false); } } /** * <p>This is a copy of the editSchema method from the GanymedeAdmin * class which has been modified so that it will assert a schema * edit lock without requiring that the login semaphore count be * zero. This way we can get a DBSchemaEdit context that we can use * to do XML-based schema editing without having to have dropped our * GanymedeSession's semaphore increment. This is safe to do only * because we know that the GanymedeXMLSession is single-threaded * and will not do any database activity while the schema is opened * for editing.</p> * * @return null if the server could not be put into schema edit mode. */ private DBSchemaEdit editSchema() { // NB: disableToken must be "schema edit:" followed by the admin // name to match logic in GanymedeServer, GanymedeAdmin, and // DBSchemaEdit String schemaDisableToken = "schema edit:" + this.session.getIdentity(); // first, let's check to see if we're the only session in, and // if we are disable the semaphore. We have to do all of this // in a block synchronized on GanymedeServer.lSemaphore so // that we won't proceed to approve the schema edit if someone // else has logged into the server synchronized (GanymedeServer.lSemaphore) { if (GanymedeServer.lSemaphore.getCount() != 1) { return null; // someone else is logged in, can't do it } // "GanymedeXMLSession entering editSchema" Ganymede.debug(ts.l("editSchema.entering")); // try disabling the semaphore with a false waitForZero value, // so that we can go into schema edit mode while still // maintaining our GanymedeSession's semaphore increment try { String semaphoreCondition = GanymedeServer.lSemaphore.disable(schemaDisableToken, false, 0); if (semaphoreCondition != null) { // "GanymedeXMLSession Can''t edit schema, semaphore error: {0}" Ganymede.debug(ts.l("editSchema.semaphore_blocked", semaphoreCondition)); return null; } } catch (InterruptedException ex) { Ganymede.logError(ex); throw new RuntimeException(ex.getMessage()); } } // okay at this point we've asserted our interest in editing the // schema and made sure that no one else is logged in or can log // in. Now we just need to make sure that we don't have any of // the bases locked by anything that is skipping the semaphore, // such as tasks. // In fact, I believe that the server is now safe against lock // races due to all tasks that might involve DBObjectBase access // being guarded by the loginSemaphore, but there is little cost // in sync'ing here. // All the DBLock establish methods synchronize on the DBLockSync // object referenced by Ganymede.db.lockSync, so we are safe // against lock establish race conditions by synchronizing this // section on Ganymede.db.lockSync. synchronized (Ganymede.db.lockSync) { // "GanymedeXMLSession entering editSchema synchronization block" Ganymede.debug(ts.l("editSchema.entering_synchronized")); for (DBObjectBase base: Ganymede.db.bases()) { if (base.isLocked()) { // "GanymedeXMLSession Can''t edit schema, previous lock held on object base {0}" Ganymede.debug(ts.l("editSchema.base_blocked", base.getName())); GanymedeServer.lSemaphore.enable(schemaDisableToken); return null; } } // should be okay // "GanymedeXMLSession Ok to create DBSchemaEdit" Ganymede.debug(ts.l("editSchema.ok_to_edit")); // "XML Schema Edit In Progress" GanymedeAdmin.setState(ts.l("editSchema.admin_notify")); try { DBSchemaEdit result = new DBSchemaEdit(this.session.getIdentity()); return result; } catch (RemoteException ex) { GanymedeServer.lSemaphore.enable(schemaDisableToken); return null; } } } /** * Private helper method to print to the client the text of * any return val dialog. Returns true if the retval codes * for success, false otherwise. */ private boolean handleReturnVal(ReturnVal retval) { if (retval != null && retval.getDialogText() != null) { tell(retval.getDialogText()); } if (ReturnVal.didSucceed(retval)) { return true; } return false; } /** * Append some output to the error stream that the client will * receive. */ public void tell(String buf) { // synchronize on the parsing semaphore so that the // getNextErrChunk() method won't have a possible race condition // between the semaphore and the err PrintWriter. // // cf. http://en.wikipedia.org/wiki/Java_Memory_Model synchronized (this.parsing) { this.err.println(buf); } } /** * Append a stack trace to the error stream that the client will * receive. */ public void tell(Throwable ex) { this.err.println(Ganymede.stackTrace(ex)); } } /*------------------------------------------------------------------------------ class XMLIntegrationException ------------------------------------------------------------------------------*/ /** * An internal exception used for flow control in * GanymedeXMLSession.integrateXMLTransaction(). */ class XMLIntegrationException extends RuntimeException { public XMLIntegrationException() { super(); } public XMLIntegrationException(String message) { super(message); } }