/* GASH 2 DBStore.java The GANYMEDE object storage system. Created: 2 July 1996 Module By: Jonathan Abbey, jonabbey@arlut.utexas.edu ----------------------------------------------------------------------- Ganymede Directory Management System Copyright (C) 1996-2014 The University of Texas at Austin Ganymede is a registered trademark of The University of Texas at Austin Contact information Web site: http://www.arlut.utexas.edu/gash2 Author Email: ganymede_author@arlut.utexas.edu Email mailing list: ganymede@arlut.utexas.edu US Mail: Computer Science Division Applied Research Laboratories The University of Texas at Austin PO Box 8029, Austin TX 78713-8029 Telephone: (512) 835-3200 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package arlut.csd.ganymede.server; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StreamTokenizer; import java.io.StringReader; import java.rmi.RemoteException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Vector; import arlut.csd.Util.JythonMap; import arlut.csd.Util.TranslationService; import arlut.csd.Util.zipIt; import arlut.csd.ganymede.common.FieldType; import arlut.csd.ganymede.common.Invid; import arlut.csd.ganymede.common.InvidPool; import arlut.csd.ganymede.common.NotLoggedInException; import arlut.csd.ganymede.common.PermEntry; import arlut.csd.ganymede.common.ReturnVal; import arlut.csd.ganymede.common.SchemaConstants; import arlut.csd.ganymede.rmi.CategoryNode; import com.jclark.xml.output.UTF8XMLWriter; /*------------------------------------------------------------------------------ class DBStore ------------------------------------------------------------------------------*/ /** * <p>DBStore is the main Ganymede database class. DBStore is * responsible for actually handling the Ganymede database, and * manages database loads and dumps, locking (in conjunction with * {@link arlut.csd.ganymede.server.DBSession DBSession} and {@link * arlut.csd.ganymede.server.DBLock DBLock}), the {@link * arlut.csd.ganymede.server.DBJournal journal}, and schema * dumping.</p> * * <p>The DBStore class holds the server's namespace and schema * dictionaries, in the form of a collection of {@link * arlut.csd.ganymede.server.DBNameSpace DBNameSpace} and {@link * arlut.csd.ganymede.server.DBObjectBase DBObjectBase} objects. Each * DBObjectBase contains schema information for an object type, * including field definitions for all fields that may be stored in * objects of that type.</p> * * <p>In addition to holding schema information, each DBObjectBase * contains an {@link arlut.csd.ganymede.common.Invid Invid}-keyed * hash of {@link arlut.csd.ganymede.server.DBObject DBObject}'s of * that type in memory after the database loading is complete at * start-up. Changes made to the DBStore are done in {@link * arlut.csd.ganymede.server.DBEditSet transactional contexts} using * DBSession, which is responsible for initiating journal changes when * individual transactions are committed to the database. * Periodically, the Ganymede server's {@link * arlut.csd.ganymede.server.GanymedeScheduler GanymedeScheduler} task * engine will schedule a full {@link * arlut.csd.ganymede.server.DBStore#dump(java.lang.String,boolean,boolean) * dump} to consolidate the journal and update the on-disk database * file. The server will also do a dump when the server's admin * console {@link arlut.csd.ganymede.server.GanymedeAdmin * GanymedeAdmin} interface initiates a server shutdown.</p> * * <p>DBStore was originally written with the intent of being able to * serve as a stand-alone in-process transactional object storage * system, but in practice, DBStore is now thoroughly integrated with * the rest of the Ganymede server, and particularly with {@link * arlut.csd.ganymede.server.GanymedeSession GanymedeSession}. * Various component classes ({@link * arlut.csd.ganymede.server.DBSession DBSession}, {@link * arlut.csd.ganymede.server.DBObject DBObject}, and {@link * arlut.csd.ganymede.server.DBField DBField}), assume that there is * usually an associated GanymedeSession to be consulted for * permissions and the like.</p> * * @author Jonathan Abbey, jonabbey@arlut.utexas.edu, ARL:UT */ public final class DBStore implements JythonMap { // type identifiers used in the object store /** * All ganymede.db files will start with this string */ static final String id_string = "Gstore"; /** * Major file version id.. will be first byte in ganymede.db file * after id_string */ static final byte major_version = 2; /** * Minor file version id.. will be second byte in ganymede.db file * after id_string */ static final byte minor_version = 23; /** * Enable/disable debug in the DBStore methods */ static boolean debug = false; /** * We're going to just have a singleton */ static DBStore db = null; /** * TranslationService object for handling string localization in * the Ganymede server. */ static final TranslationService ts = TranslationService.getTranslationService("arlut.csd.ganymede.server.DBStore"); /** * Translated string for normal operation. * * One of the values that can be shown as 'Server State' on the * admin console. */ // "Normal Operation" public static final String normal_state = ts.l("init.okaystate"); /** * Monotonically increasing transaction number. */ private int transactionNumber = 0; /** * Object we're using as a monitor for atomically increasing * transactionNumber. */ private Object transactionNumberLock = new Object(); /** * An unmodifiable List of DBObjectBases that need to be locked by * {@link arlut.csd.ganymede.server.DBPermissionManager * DBPermissionManager} when doing permissions gathering. */ private List<DBObjectBase> permObjectBases; /** * <p>Convenience function to find and return objects from the * database without having to go through the GanymedeSession and * DBSession layers.</p> * * <p>This method provides a direct reference to an object in the * DBStore, without exporting it for remote RMI reference and * without doing any permissions setting.</p> */ public static DBObject viewDBObject(Invid invid) { if (DBStore.db == null) { // "DBStore not initialized" throw new IllegalStateException(ts.l("global.notinit")); } DBObjectBase base; base = DBStore.db.getObjectBase(invid.getType()); if (base == null) { return null; } return base.getObject(invid); } /** * Convenience synchronized function to set the singleton DBStore * db member in the DBStore class. */ private static synchronized void setDBSingleton(DBStore store) { if (DBStore.db != null) { // "DBStore already created" throw new IllegalStateException(ts.l("setDBSingleton.exception")); } DBStore.db = store; } /* --- */ /* All of the following should only be modified/accessed in a critical section synchronized on the DBStore object. */ /** * Mapping of short object types to objectBases. */ Map<Short,DBObjectBase> objectBases; /** * Tracks invids which point to specific objects via asymmetric * links in the Ganymede persistent data store. */ DBLinkTracker aSymLinkTracker; /** * A collection of {@link arlut.csd.ganymede.server.DBNameSpace * DBNameSpaces} registered in this DBStore. */ List<DBNameSpace> nameSpaces; /** * if true, {@link arlut.csd.ganymede.server.DBObjectBase DBObjectBase} * set methods will be enabled */ private boolean loading = false; /** * Root of the category tree defined in this database */ DBBaseCategory rootCategory; /** * Major revision number of the database file loaded by this * DBStore object. */ byte file_major; /** * Minor revision number of the database file loaded by this * DBStore object. */ byte file_minor; /** * The Journal for this database, initialized when the database is * loaded. */ DBJournal journal = null; /** * A separate object to act as a synchronization monitor for DBLock * code. */ public DBLockSync lockSync = new DBLockSync(); /** * We use this boolean to work around a design flaw in password * field storage in DBStore revision 2.19. */ public boolean journalLoading = false; /** * A count of how many database objects in this DBStore are currently * checked out for creation, editing, or removal. */ int objectsCheckedOut = 0; /* -- */ /** * <p>This is the constructor for DBStore.</p> * * <p>Currently, once you construct a DBStore object, all you can do to * initialize it is call load(). This API needs to be extended to * provide for programmatic bootstrapping, or another tool needs * to be produced for the purpose.</p> */ public DBStore() { debug = Ganymede.debug; objectBases = new HashMap<Short, DBObjectBase>(20); // default aSymLinkTracker = new DBLinkTracker(); nameSpaces = new ArrayList<DBNameSpace>(); try { rootCategory = new DBBaseCategory(this, ts.l("init.rootcategory")); // "Categories" } catch (RemoteException ex) { // we shouldn't ever get here unless there is something wrong // with the RMI system or the Java environment // "Caught DBBaseCategory exception: {0}" System.err.println(ts.l("init.exception", ex.getMessage())); Ganymede.logError(ex); // "Couldn''t initialize rootCategory" throw new Error(ts.l("init.error")); // XXX wrap earlier exception if we are JDK 1.4+ only? } // set the static pointer after we have at least created the basic // data structures. setDBSingleton(this); GanymedeAdmin.setState(DBStore.normal_state); // "Normal Operation" } /** * This method returns true if the disk file being loaded by this DBStore * has a version number greater than or equal to major.minor. */ public boolean isAtLeast(int major, int minor) { return (this.file_major > major || (this.file_major == major && this.file_minor >= minor)); } /** * This method returns true if the disk file being loaded by this DBStore * has a version number earlier than major.minor. */ public boolean isLessThan(int major, int minor) { return (this.file_major < major || (this.file_major == major && this.file_minor < minor)); } /** * This method returns true if the disk file being loaded by this DBStore * has a version number equal to major.minor. */ public boolean isAtRev(int major, int minor) { return (this.file_major == major && this.file_minor == minor); } /** * This method returns true if the disk file being loaded by this * DBStore has a version number between greater than or equal to * major1.minor1 and less than major2.minor2. */ public boolean isBetweenRevs(int major1, int minor1, int major2, int minor2) { if (this.file_major == major1 && this.file_minor >= minor1) { return true; } if (this.file_major == major2 && this.file_minor < minor2) { return true; } if (this.file_major > major1 && this.file_major < major2) { return true; } return false; } /** * Returns true if we're in the middle of loading our database from * disk. */ public boolean isLoading() { return loading; } /** * <p>Load the database from disk.</p> * * <p>This method loads both the database type definition and database * contents from a single disk file.</p> * * @param filename Name of the database file * @see arlut.csd.ganymede.server.DBJournal */ public synchronized void load(String filename) { FileInputStream inStream = null; BufferedInputStream bufStream = null; DataInputStream in; DBObjectBase tempBase; short baseCount, namespaceCount; int invidPoolSize; String file_id; debug = false; /* -- */ loading = true; nameSpaces.clear(); try { inStream = new FileInputStream(filename); bufStream = new BufferedInputStream(inStream); in = new DataInputStream(bufStream); try { file_id = in.readUTF(); if (!file_id.equals(id_string)) { System.err.println(ts.l("load.versionfail", filename)); throw new RuntimeException(ts.l("load.initerror", filename)); } } catch (IOException ex) { System.err.println("DBStore initialization error: DBStore id read failure for " + filename); System.err.println("IOException: "); Ganymede.logError(ex); throw new RuntimeException(ts.l("load.initerror", filename)); } file_major = in.readByte(); file_minor = in.readByte(); debug("DBStore load(): file version " + file_major + "." + file_minor); if (file_major > major_version) { System.err.println("DBStore initialization error: major version mismatch"); throw new Error("DBStore initialization error (" + filename + ")"); } if (file_major == major_version && file_minor > minor_version) { System.err.println("*** Error, this ganymede.db file is too new for this version of the Ganymede server."); System.err.println("*** There may be errors in loading the data."); } // at version 2.8, we started tracking monotonic transaction // id numbers if (this.isAtLeast(2, 8)) { transactionNumber = in.readInt(); } // at version 2.9, we started recording the size of our invid // pool if (this.isAtLeast(2, 9)) { invidPoolSize = in.readInt(); } else { invidPoolSize = -1; } if (invidPoolSize != -1) { Invid.setAllocator(new InvidPool(invidPoolSize*2 + 1)); // let's make sure we don't immediately rehash } else { Invid.setAllocator(new InvidPool()); } // read in the namespace definitions namespaceCount = in.readShort(); // "DBStore.load(): loading {0,number,#} namespaces" debug(ts.l("load.namespaces", Integer.valueOf(namespaceCount))); for (int i = 0; i < namespaceCount; i++) { nameSpaces.add(new DBNameSpace(in)); } // "DBStore.load(): loading category definitions" debug(ts.l("load.categories")); DBField.fieldCount = 0; DBObject.objectCount = 0; if (isAtLeast(1,3)) { rootCategory = new DBBaseCategory(this, in); } // previous to 2.0, we wrote out the DBObjectBase structures // to ganymede.db as a separate step from the category loading if (isLessThan(2,0)) { baseCount = in.readShort(); debug("DBStore load(): loading " + baseCount + " bases"); if (baseCount > 0) { objectBases = new Hashtable<Short, DBObjectBase>(baseCount); } else { objectBases = new Hashtable<Short, DBObjectBase>(); } // Actually read in the object bases for (short i = 0; i < baseCount; i++) { tempBase = new DBObjectBase(in, this); setBase(tempBase); debug("loaded base " + tempBase.getTypeID()); } // we're only using the resort() method because we're // handling an old file that might not have the categories // in display order. This code is intended to fall out of // use. rootCategory.resort(); } // print out loading statistics // "DBStore.load(): Loaded {0,number,#} fields in {1,number,#} objects" debug(ts.l("load.statistics", Integer.valueOf(DBField.fieldCount), Integer.valueOf(DBObject.objectCount))); // make sure that we have the new object bases and fields // that have come into use since Ganymede 1.0.12. verifySchema2_0(); // make sure that all object bases have namespace-constrained // label fields. This is a new requirement with Ganymede 2.0 // (DBStore rev 2.11), and not one that we can really automate // in any way. So we just warn the admin and hope that they // start up the schema editor so that we can force them to fix // it. if (!verify_label_fields()) { /* * * * WARNING: DBStore load error: one or more object bases are missing * namespace constrained label fields. * * Ganymede 2.0 now requires all object types to have * namespace-constrained label fields. * * You MUST edit the schema before proceeding and define label fields * for all object types, or else Ganymede will behave unreliably. * */ System.err.println(ts.l("load.missing_labels")); } } catch (IOException ex) { System.err.println("DBStore initialization error: couldn't properly process " + filename); System.err.println("IOException: " + ex); System.err.println("Stack Trace: " + Ganymede.stackTrace(ex)); throw new RuntimeException(ts.l("load.initerror", filename)); } finally { if (bufStream != null) { try { bufStream.close(); } catch (IOException ex) { } } if (inStream != null) { try { inStream.close(); } catch (IOException ex) { } } } // Load the current Journal file. loadJournal(); // go ahead and consolidate the journal into the DBStore // before we really get under way. // Or if the old DB version does not match the new // Notice that we are going to archive a copy of the // existing db file. if (!isAtRev(major_version, minor_version) || !journal.isClean()) { try { Ganymede.debug("Coalescing DBJournal."); dump(filename, true, true); } catch (IOException ex) { // what do we really want to do here? Ganymede.logError(ex); throw new RuntimeException("couldn't load journal"); } catch (InterruptedException ex) { // we got interrupted while waiting to lock // the database.. unlikely in the extreme here. Ganymede.logError(ex); throw new RuntimeException("couldn't dump journal"); } } loading = false; } /** * <p>Loads the current Journal file.</p> * */ public synchronized void loadJournal() { try { journal = new DBJournal(this, Ganymede.journalProperty); } catch (IOException ex) { // what do we really want to do here? Ganymede.logError(ex); throw new RuntimeException("couldn't initialize journal:" + ex.getMessage()); } if (!journal.isClean()) { try { journalLoading = true; if (!journal.load()) { // if the journal wasn't in a totally consistent // state, print out a warning. we'll still // continue to do everything we normally would, // however. System.err.println("\nError, couldn't load entire journal.. " + "final transaction in journal not processed.\n"); } // update the DBObjectBase iterationSets, since the journal // loading bypasses the transaction mechanism where this is // normally done for (DBObjectBase base: objectBases.values()) { base.updateIterationSet(); } } catch (IOException ex) { // what do we really want to do here? Ganymede.logError(ex); throw new RuntimeException("couldn't load journal"); } finally { journalLoading = false; } } } /** * <p>Dumps the database to disk</p> * * <p>This method dumps the entire database to disk. The thread that calls the * dump method will be suspended until there are no threads performing update * writes to the in-memory database. In practice this will likely never be * a long interval. Note that this method *will* dump the database, even * if no changes have been made. You should check the DBStore journal's * isClean() method to determine whether or not a dump is really needed, * if you're not sure.</p> * * <p>The dump is guaranteed to be transaction consistent.</p> * * @param filename Name of the database file to emit * @param releaseLock boolean. If releaseLock==false, dump() will not release * the dump lock when it is done with the dump. This * is intended to allow for a clean shut down. For * non-terminal dumps, releaseLock should be true. * @param archiveIt If true, dump will create a zipped copy of the previously existing * ganymede.db file in an 'old' directory under the location where * ganymede.db is held. * * @see arlut.csd.ganymede.server.DBEditSet * @see arlut.csd.ganymede.server.DBJournal */ public synchronized void dump(String filename, boolean releaseLock, boolean archiveIt) throws IOException, InterruptedException { File dbFile = null; FileOutputStream outStream = null; BufferedOutputStream bufStream = null; DataOutputStream out = null; short namespaceCount; DBDumpLock lock = null; /* -- */ debug("DBStore: Dumping"); lock = new DBDumpLock(this); debug("DBStore: establishing dump lock"); lock.establish("System"); // wait until we get our lock debug("DBStore: dump lock established"); // Move the old version of the file to a backup try { dbFile = new File(filename); // first thing we do is zip up the old ganymede.db file if // archiveIt is true. if (dbFile.exists()) { if (archiveIt) { debug("DBStore: zipping old db"); String directoryName = dbFile.getParent(); File directory = new File(directoryName); if (!directory.isDirectory()) { throw new IOException("Error, couldn't find output directory to backup: " + directoryName); } String oldDirName = directoryName + File.separator + "old"; File oldDirectory = new File(oldDirName); if (!oldDirectory.exists()) { if (!oldDirectory.mkdir()) { throw new IOException("Couldn't mkdir " + oldDirName); } } DateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss", java.util.Locale.US); String label = formatter.format(new Date()); String zipFileName = directoryName + File.separator + "old" + File.separator + label + "db.zip"; Vector fileNameVect = new Vector(); fileNameVect.addElement(filename); zipIt.createZipFile(zipFileName, fileNameVect); } } debug("DBStore: writing new db"); // and dump the whole thing to ganymede.db.new outStream = new FileOutputStream(filename + ".new"); bufStream = new BufferedOutputStream(outStream); out = new DataOutputStream(bufStream); // okay, then! let's get started! out.writeUTF(id_string); out.writeByte(major_version); out.writeByte(minor_version); out.writeInt(transactionNumber); // added in version 2.8 out.writeInt(Invid.getCount()); // added in version 2.9 namespaceCount = (short) nameSpaces.size(); out.writeShort(namespaceCount); for (DBNameSpace ns: nameSpaces) { ns.emit(out); } rootCategory.emit(out); // writes out categories and bases out.flush(); outStream.getFD().sync(); out.close(); out = null; if (dbFile.exists()) { // ok, we've successfully dumped to ganymede.db.new.. move // the old file to ganymede.db.bak if (!dbFile.renameTo(new File(filename + ".bak"))) { throw new IOException("Couldn't rename " + filename + " to " + filename + ".bak"); } } // and move ganymede.db.new to ganymede.db.. note that we do // have a very slight vulnerability here if we are interrupted // between the above rename and this one. If this happens, // the Ganymede manager will have to manually move // ganymede.db.new to ganymede.db before starting the server. // In all other circumstances, the server will be able to come // up and handle things cleanly without loss of data. debug("DBStore: renaming new db"); File newFile = new File(filename + ".new"); if (!newFile.renameTo(dbFile)) { throw new IOException("Couldn't rename " + filename + ".new to " + filename); } } catch (IOException ex) { System.err.println("DBStore error dumping to " + filename); throw ex; } finally { if (releaseLock) { if (lock != null) { debug("DBStore: releasing dump lock"); lock.release(); } } if (out != null) { out.close(); } if (bufStream != null) { bufStream.close(); } if (outStream != null) { outStream.close(); } } // if our thread was terminated above, we won't get here. If // we've got here, we had an ok dump, and we don't need to // worry about the journal.. if it isn't truncated properly // somebody can just remove it and ganymede will recover // ok. if (journal != null) { journal.reset(); // ** sync ** } GanymedeAdmin.updateLastDump(new Date()); if (Ganymede.log != null) { Ganymede.log.logSystemEvent(new DBLogEvent("dump", "Database dumped", null, null, null, null)); } } /** * <p>Dumps the database to disk as an XML file</p> * * <p>This method dumps the entire database to disk. The thread that calls the * dump method will be suspended until there are no threads performing update * writes to the in-memory database. In practice this will likely never be * a long interval. Note that this method *will* dump the database, even * if no changes have been made. You should check the DBStore journal's * isClean() method to determine whether or not a dump is really needed, * if you're not sure.</p> * * <p>The dump is guaranteed to be transaction consistent.</p> * * @param filename Name of the database file to emit * * @param dumpDataObjects If true, the emitted file will include * the objects in the Ganymede database. * * @param dumpSchema If true, the emitted file will include the * schema definition * * @param syncChannel The name of the sync channel whose constraints * we want to apply to this dump. May be null if the client wants * an unfiltered dump. * * @param includeHistory If true, the historical fields (creation * date & info, last modification date & info) will be * included in the xml stream. * * @see arlut.csd.ganymede.server.DBEditSet * @see arlut.csd.ganymede.server.DBJournal */ public void dumpXML(String filename, boolean dumpDataObjects, boolean dumpSchema, String syncChannel, boolean includeHistory, boolean includeOid) throws IOException { FileOutputStream outStream = null; BufferedOutputStream bufStream = null; /* -- */ outStream = new FileOutputStream(filename); bufStream = new BufferedOutputStream(outStream); this.dumpXML(bufStream, dumpDataObjects, dumpSchema, syncChannel, includeHistory, includeOid); } /** * <p>Dumps the database and/or database schema to an OutputStream * as an XML file</p> * * <p>This method dumps the entire database to the OutputStream. * The thread that calls the dump method will be suspended until * there are no threads performing update writes to the in-memory * database. In practice this will likely never be a long interval. * Note that this method *will* dump the database, even if no * changes have been made. You should check the DBStore journal's * isClean() method to determine whether or not a dump is really * needed, if you're not sure.</p> * * <p>The dump is guaranteed to be transaction consistent.</p> * * @param outStream Stream to write the XML to @param * dumpDataObjects if false, only the schema definition will be * written * * @param dumpDataObjects If true, the emitted file will include * the objects in the Ganymede database. * * @param dumpSchema If true, the emitted file will include the * schema definition * * @param syncChannel The name of the sync channel whose constraints * we want to apply to this dump. May be null if the client wants * an unfiltered dump. * * @param includeHistory If true, the historical fields (creation * date & info, last modification date & info) will be * included in the xml stream. * * @param includeOid If true, the objects written out to the xml * stream will include an "oid" attribute which contains the precise * Invid of the object. * * @see arlut.csd.ganymede.server.DBEditSet * @see arlut.csd.ganymede.server.DBJournal */ public synchronized void dumpXML(OutputStream outStream, boolean dumpDataObjects, boolean dumpSchema, String syncChannel, boolean includeHistory, boolean includeOid) throws IOException { XMLDumpContext xmlOut = null; DBDumpLock lock = null; DBNameSpace ns; SyncRunner syncConstraint = null; boolean includePlaintext = false; /* -- */ debug("DBStore: Dumping XML"); try { if (!dumpDataObjects && !dumpSchema) { // "One of dumpDataObjects and dumpSchema must be true." throw new IllegalArgumentException(ts.l("dumpXML.doNothing")); } // we don't need a lock if we're just dumping the schema, // because the GanymedeServer loginSemaphore will keep the // schema from changing under us. if (!dumpDataObjects) { xmlOut = new XMLDumpContext(new UTF8XMLWriter(outStream, UTF8XMLWriter.MINIMIZE_EMPTY_ELEMENTS)); dumpSchemaXML(xmlOut); return; // but see finally {} below for cleanup } lock = new DBDumpLock(this); try { lock.establish("System"); // wait until we get our lock } catch (InterruptedException ex) { // "DBStore.dumpXML(): Interrupted waiting for dump lock:\n\n{0}" Ganymede.debug(ts.l("dumpXML.interrupted", Ganymede.stackTrace(ex))); throw new RuntimeException(ex); } if (false) { System.err.println("DBStore.dumpXML(): got dump lock"); } if (dumpDataObjects && !dumpSchema && syncChannel != null) { syncConstraint = Ganymede.getSyncChannel(syncChannel); if (syncConstraint == null) { // "No such sync channel defined: {0}" throw new IllegalArgumentException(ts.l("dumpXML.badSyncChannel", syncChannel)); } else { includePlaintext = syncConstraint.includePlaintextPasswords(); } } xmlOut = new XMLDumpContext(new UTF8XMLWriter(outStream, UTF8XMLWriter.MINIMIZE_EMPTY_ELEMENTS), includePlaintext, includeHistory, syncConstraint, includeOid); // we're doing a supergash-approved full dump, so passwords are allowed xmlOut.setDumpPasswords(true); if (false) { System.err.println("DBStore.dumpXML(): created XMLDumpContext"); } // start writing xmlOut.startElement("ganymede"); xmlOut.attribute("major", Integer.toString(GanymedeXMLSession.majorVersion)); xmlOut.attribute("minor", Integer.toString(GanymedeXMLSession.minorVersion)); xmlOut.indentOut(); if (dumpSchema) { // we may be dumping the schema along with the data objects dumpSchemaXML(xmlOut); } xmlOut.startElementIndent("ganydata"); xmlOut.indentOut(); for (DBObjectBase base: objectBases.values()) { if (base.isEmbedded()) { continue; } for (DBObject x: base.getObjects()) { if (xmlOut.mayInclude(x)) { x.emitXML(xmlOut); } } } xmlOut.indentIn(); xmlOut.endElementIndent("ganydata"); xmlOut.indentIn(); xmlOut.endElementIndent("ganymede"); xmlOut.write("\n"); xmlOut.close(); xmlOut = null; } finally { if (false) { System.err.println("DBStore.dumpXML(): finally!"); } if (lock != null) { lock.release(); } if (xmlOut != null) { try { xmlOut.close(); } catch (IOException ex) { Ganymede.logError(ex); } } if (outStream != null) { try { outStream.close(); } catch (IOException ex) { Ganymede.logError(ex); } } } } /** * Dumps the schema definition to xmlOut. */ public synchronized void dumpSchemaXML(XMLDumpContext xmlOut) throws IOException { xmlOut.startElementIndent("ganyschema"); xmlOut.indentOut(); xmlOut.startElementIndent("namespaces"); xmlOut.indentOut(); for (DBNameSpace ns: nameSpaces) { ns.emitXML(xmlOut); } xmlOut.indentIn(); xmlOut.endElementIndent("namespaces"); // write out our category tree xmlOut.skipLine(); xmlOut.startElementIndent("object_type_definitions"); xmlOut.indentOut(); rootCategory.emitXML(xmlOut); xmlOut.indentIn(); xmlOut.endElementIndent("object_type_definitions"); xmlOut.indentIn(); xmlOut.endElementIndent("ganyschema"); } /** * Returns an immutable List of Strings, the names of the bases * currently defined in this DBStore. */ public List<String> getBaseNameList() { List<String> result = new ArrayList<String>(); /* -- */ synchronized (objectBases) { for (DBObjectBase base: objectBases.values()) { result.add(base.getName()); } } return Collections.unmodifiableList(result); } /** * Returns an immutable List copy of the {@link * arlut.csd.ganymede.server.DBObjectBase DBObjectBases} currently * defined in this DBStore. */ public List<DBObjectBase> getBases() { List<DBObjectBase> result; synchronized (objectBases) { result = new ArrayList<DBObjectBase>(objectBases.values()); } return Collections.unmodifiableList(result); } /** * <p>Returns an immutable directly iterable Collection view of the * {@link arlut.csd.ganymede.server.DBObjectBase DBObjectBases} * currently defined in this DBStore.</p> * * <p>No copy is made, this is a direct view.</p> */ public Collection<DBObjectBase> bases() { return Collections.unmodifiableCollection(objectBases.values()); } /** * Returns the object definition class for the id class. * * @param id Type id for the base to be returned */ public DBObjectBase getObjectBase(Short id) { return objectBases.get(id); } /** * Returns the object definition class for the id class. * * @param id Type id for the base to be returned */ public DBObjectBase getObjectBase(short id) { return objectBases.get(Short.valueOf(id)); } /** * Returns the object definition class for the id class. * * @param baseName Name of the base to be returned */ public DBObjectBase getObjectBase(String baseName) { synchronized (objectBases) { for (DBObjectBase base: objectBases.values()) { if (base.getName().equalsIgnoreCase(baseName)) { return base; } } } return null; } /** * Returns an immutable List of valid DBObjectBase objects that need * to be locked by {@link * arlut.csd.ganymede.server.DBPermissionManager * DBPermissionManager} when it gathers permissions in {@link * arlut.csd.ganymede.server.DBPermissionManager#updatePerms()} */ public List<DBObjectBase> getPermBases() { synchronized (objectBases) { if (permObjectBases != null) { return permObjectBases; } List<DBObjectBase> basesToLock = (List<DBObjectBase>) new ArrayList(); basesToLock.add(getObjectBase(SchemaConstants.OwnerBase)); basesToLock.add(getObjectBase(SchemaConstants.RoleBase)); basesToLock.add(getObjectBase(SchemaConstants.PersonaBase)); permObjectBases = Collections.unmodifiableList(basesToLock); return permObjectBases; } } /** * Performs book-keeping when a schema edit is committed. */ public void finishSchemaEditCommit() { this.permObjectBases = null; } /** * Method to replace/add a DBObjectBase in the DBStore. */ public synchronized void setBase(DBObjectBase base) { base.setEditingMode(DBObjectBase.EditingMode.LOCKED); objectBases.put(base.getKey(), base); } /** * Increments and returns the server's monotonic * transactionNumber. */ public int getNextTransactionNumber() { synchronized (transactionNumberLock) { return ++transactionNumber; } } /** * Returns the most recent transaction number allocated. */ public int getTransactionNumber() { return transactionNumber; } /** * This method is used by the * {@link arlut.csd.ganymede.server.DBJournal#undoTransaction(arlut.csd.ganymede.server.DBJournalTransaction)} * method to put back a transaction number when it is undone. */ public void undoNextTransactionNumber(int number) { synchronized (transactionNumberLock) { if (number == transactionNumber) { --transactionNumber; } } } /** * This method is used when reading journal entries to * bump up the transaction number. If the nextNumber provided * isn't actually the next number in our transaction sequence, * we'll throw an IntegrityConstraintException. */ public void updateTransactionNumber(int nextNumber) throws IntegrityConstraintException { synchronized (transactionNumberLock) { if (transactionNumber > 0 && nextNumber != transactionNumber + 1) { if (nextNumber == transactionNumber) { // Inconsistent transaction number ({0}) detected while reading transaction from journal, expected {1}. // This probably means that the server was terminated in the middle of a dump process, // and that the journal file is older than the ganymede.db file. You should be able to // remove the journal file and restart the server. throw new IntegrityConstraintException(ts.l("updateTransactionNumber.lingeringJournal", Integer.valueOf(nextNumber), Integer.valueOf(transactionNumber+1))); } else { // "Inconsistent transaction number ({0}) detected while reading transaction from journal, expected {1}. Throwing up." throw new IntegrityConstraintException(ts.l("updateTransactionNumber.badnumber", Integer.valueOf(nextNumber), Integer.valueOf(transactionNumber+1))); } } transactionNumber = nextNumber; } } /** * <p>Method to obtain the object from the DBStore represented by the * given Invid.</p> * * <p>NOTE: this method will give you a direct reference to the actual * DBObject, not a clone. So be careful, and treat the returned * objects as "read only".</p> */ public DBObject getObject(Invid invid) { return getObjectBase(invid.getType()).getObject(invid.getNum()); } /** * Method to locate a registered namespace by name. */ public DBNameSpace getNameSpace(String name) { synchronized (nameSpaces) { for (DBNameSpace namespace: nameSpaces) { if (namespace.getName().equalsIgnoreCase(name)) { return namespace; } } } return null; } /** * Method to get a category from the category list, by * it's full path name. */ public synchronized CategoryNode getCategoryNode(String pathName) { DBBaseCategory bc; int tok; /* -- */ if (pathName == null) { throw new IllegalArgumentException("can't deal with null pathName"); } // System.err.println("DBStore.getCategory(): searching for " + pathName); StringReader reader = new StringReader(pathName); StreamTokenizer tokens = new StreamTokenizer(reader); tokens.wordChars(Integer.MIN_VALUE, Integer.MAX_VALUE); tokens.ordinaryChar('/'); tokens.slashSlashComments(false); tokens.slashStarComments(false); try { tok = tokens.nextToken(); bc = rootCategory; // The path is going to include the name of the root node // itself (unlike in the UNIX filesystem, where the root node // has no 'name' of its own), so we need to skip into the // root node. if (tok == '/') { tok = tokens.nextToken(); } if (tok == StreamTokenizer.TT_WORD && tokens.sval.equals(rootCategory.getName())) { tok = tokens.nextToken(); } while (tok != StreamTokenizer.TT_EOF && bc != null) { // note that slashes are the only non-word token we // should ever get, so they are implicitly separators. if (tok == StreamTokenizer.TT_WORD) { // System.err.println("DBStore.getCategory(): Looking for node " + tokens.sval); CategoryNode cn = bc.getNode(tokens.sval); if (cn instanceof DBBaseCategory) { bc = (DBBaseCategory) cn; } else if (cn instanceof DBObjectBase) { return cn; } else { throw new RuntimeException("Found unknown/null thing in category tree.." + cn.toString()); } } tok = tokens.nextToken(); } } catch (IOException ex) { throw new RuntimeException("parse error in getCategory: " + ex); } return bc; } /** * <p>Initialization method for a newly created DBStore.. this * method creates a new Schema from scratch, defining the * mandatory Ganymede object types, registering their customization * classes, defining fields, and all the rest.</p> * * <p>Note that we don't go through a * {@link arlut.csd.ganymede.server.DBSchemaEdit DBSchemaEdit} * here, we just initialize the {@link arlut.csd.ganymede.server.DBObjectBase DBObjectBase}/ * {@link arlut.csd.ganymede.server.DBObjectBaseField DBObjectBaseField} structures * manually.</p> */ void initializeSchema() { DBObjectBase b; DBObjectBaseField bf; DBNameSpace ns; /* -- */ loading = true; try { DBBaseCategory adminCategory = new DBBaseCategory(this, "Admin-Level Objects", rootCategory); rootCategory.addNodeAfter(adminCategory, null); ns = new DBNameSpace("ownerbase", true); nameSpaces.add(ns); ns = new DBNameSpace("username", true); nameSpaces.add(ns); ns = new DBNameSpace("rolespace", true); nameSpaces.add(ns); ns = new DBNameSpace("persona", true); nameSpaces.add(ns); ns = new DBNameSpace("eventtoken", true); nameSpaces.add(ns); ns = new DBNameSpace("buildertask", true); nameSpaces.add(ns); DBBaseCategory permCategory = new DBBaseCategory(this, "Permissions", adminCategory); adminCategory.addNodeAfter(permCategory, null); // create owner base b = new DBObjectBase(this, false); b.setName("Owner Group"); b.setClassInfo("arlut.csd.ganymede.server.ownerCustom", null); b.setTypeID(SchemaConstants.OwnerBase); // 0 permCategory.addNodeAfter(b, null); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.OwnerNameField); bf.setType(FieldType.STRING); bf.setName("Name"); bf.setNameSpace("ownerbase"); bf.setComment("The name of this ownership group"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.OwnerMembersField); bf.setType(FieldType.INVID); bf.setName("Members"); bf.setArray(true); bf.setTargetBase(SchemaConstants.PersonaBase); bf.setTargetField(SchemaConstants.PersonaGroupsField); bf.setComment("List of admin personae that are members of this owner set"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.OwnerCcAdmins); bf.setType(FieldType.BOOLEAN); bf.setName("Cc All Admins"); bf.setComment("If checked, mail to this owner group will be sent to the admins"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.OwnerExternalMail); bf.setType(FieldType.STRING); bf.setName("External Mail List"); bf.setArray(true); bf.setComment("What external email addresses should be notified of changes to objects owned?"); b.addFieldToEnd(bf); /* XXX As of DBStore 2.7, we don't use this field any more bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.OwnerObjectsOwned); bf.setType(FieldType.INVID); bf.setName("Objects owned"); bf.setTargetBase(-2); // any bf.setTargetField(SchemaConstants.OwnerListField); bf.setArray(true); bf.setComment("What objects are owned by this owner set"); b.addFieldToEnd(bf); */ b.setLabelField(SchemaConstants.OwnerNameField); // and link in the base to this DBStore setBase(b); // create persona base b = new DBObjectBase(this, false); b.setName("Admin Persona"); b.setClassInfo("arlut.csd.ganymede.server.adminPersonaCustom", null); b.setTypeID(SchemaConstants.PersonaBase); // 1 permCategory.addNodeAfter(b, null); // add it to the end is ok bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.PersonaNameField); bf.setType(FieldType.STRING); bf.setName("Name"); bf.setComment("Descriptive label for this admin persona"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.PersonaPasswordField); bf.setType(FieldType.PASSWORD); bf.setName("Password"); bf.setMaxLength((short)32); bf.setCrypted(false); bf.setShaUnixCrypted(true); bf.setShaUnixCrypted512(true); bf.setComment("Persona password"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.PersonaGroupsField); bf.setType(FieldType.INVID); bf.setName("Owner Sets"); bf.setTargetBase(SchemaConstants.OwnerBase); bf.setTargetField(SchemaConstants.OwnerMembersField); bf.setArray(true); bf.setComment("What owner sets are this persona members of?"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.PersonaAssocUser); bf.setType(FieldType.INVID); bf.setName("User"); bf.setTargetBase(SchemaConstants.UserBase); bf.setTargetField(SchemaConstants.UserAdminPersonae); bf.setArray(false); bf.setComment("What user is this admin persona associated with?"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.PersonaPrivs); bf.setType(FieldType.INVID); bf.setName("Privilege Sets"); bf.setTargetBase(SchemaConstants.RoleBase); bf.setTargetField(SchemaConstants.RolePersonae); bf.setArray(true); bf.setComment("What permission matrices are this admin persona associated with?"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.PersonaAdminConsole); bf.setType(FieldType.BOOLEAN); bf.setName("Admin Console"); bf.setArray(false); bf.setComment("If true, this persona can be used to access the admin console"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.PersonaAdminPower); bf.setType(FieldType.BOOLEAN); bf.setName("Full Console"); bf.setArray(false); bf.setComment("If true, this persona can kill users and edit the schema"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.PersonaMailAddr); bf.setType(FieldType.STRING); bf.setName("Email Address"); bf.setArray(false); bf.setComment("Where email to this administrator should be sent"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.PersonaLabelField); bf.setType(FieldType.STRING); bf.setName("Label"); bf.setArray(false); bf.setNameSpace("persona"); b.addFieldToEnd(bf); b.setLabelField(SchemaConstants.PersonaLabelField); // and link in the base to this DBStore setBase(b); // create Role base b = new DBObjectBase(this, false); b.setName("Role"); b.setClassInfo("arlut.csd.ganymede.server.permCustom", null); b.setTypeID(SchemaConstants.RoleBase); // 2 permCategory.addNodeAfter(b, null); // add it to the end is ok bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.RoleName); bf.setType(FieldType.STRING); bf.setName("Name"); bf.setNameSpace("rolespace"); bf.setComment("The name of this permission matrix"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.RoleDelegatable); bf.setType(FieldType.BOOLEAN); bf.setName("Delegatable Role"); bf.setComment("If true, this role can be granted to admins created/edited by Personae with this role."); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.RoleMatrix); bf.setType(FieldType.PERMISSIONMATRIX); bf.setName("Objects Owned Access Bits"); bf.setComment("Access bits, by object type for objects owned by admins using this permission object"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.RoleDefaultMatrix); bf.setType(FieldType.PERMISSIONMATRIX); bf.setName("Default Access Bits"); bf.setComment("Access bits, by object type for all objects on the part of admins using this permission object"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.RolePersonae); bf.setType(FieldType.INVID); bf.setName("Persona entities"); bf.setTargetBase(SchemaConstants.PersonaBase); bf.setTargetField(SchemaConstants.PersonaPrivs); bf.setArray(true); bf.setComment("What personae are using this permission matrix?"); b.addFieldToEnd(bf); b.setLabelField(SchemaConstants.RoleName); // and link in the base to this DBStore setBase(b); // create System Event base DBBaseCategory eventCategory = new DBBaseCategory(this, "Events", adminCategory); adminCategory.addNodeAfter(eventCategory, null); b = new DBObjectBase(this, false); b.setName("System Event"); b.setClassInfo("arlut.csd.ganymede.server.eventCustom", null); b.setTypeID(SchemaConstants.EventBase); eventCategory.addNodeAfter(b, null); // add it to the end is ok bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.EventToken); bf.setType(FieldType.STRING); bf.setName("Event Token"); bf.setBadChars(" :"); bf.setNameSpace("eventtoken"); bf.setComment("Single-word token to identify this event type in Ganymede source code"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.EventName); bf.setType(FieldType.STRING); bf.setName("Event Name"); bf.setBadChars(":"); bf.setComment("Short name for this event class, suitable for an email message title"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.EventDescription); bf.setType(FieldType.STRING); bf.setName("Event Description"); bf.setBadChars(":"); bf.setComment("Fuller description for this event class, suitable for an email message body"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.EventMailBoolean); bf.setType(FieldType.BOOLEAN); bf.setName("Send Mail"); bf.setComment("If true, occurrences of this event will be emailed"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.EventMailToSelf); bf.setType(FieldType.BOOLEAN); bf.setName("Cc Admin"); bf.setComment("If true, mail for this event will always be cc'ed to the admin performing the action"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.EventMailOwners); bf.setType(FieldType.BOOLEAN); bf.setName("Cc Owners"); bf.setComment("If true, mail for this event will always be cc'ed to administrators in the owner group"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.EventExternalMail); bf.setType(FieldType.STRING); bf.setName("Mail List"); bf.setArray(true); bf.setComment("List of email addresses to send this event to"); b.addFieldToEnd(bf); b.setLabelField(SchemaConstants.EventToken); // and link in the base to this DBStore setBase(b); // create Object Event base b = new DBObjectBase(this, false); b.setName("Object Event"); b.setClassInfo("arlut.csd.ganymede.server.objectEventCustom", null); b.setTypeID(SchemaConstants.ObjectEventBase); eventCategory.addNodeAfter(b, null); // add it to the end is ok bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.ObjectEventLabel); bf.setType(FieldType.STRING); bf.setName("Hidden Label"); bf.setNameSpace("eventtoken"); bf.setVisibility(false); // hidden bf.setComment("Hidden composite label field. The contents of this label field is automatically set from the Event Token and Object Type Name fields."); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.ObjectEventToken); bf.setType(FieldType.STRING); bf.setName("Event Token"); bf.setBadChars(" :"); bf.setComment("Single-word token to identify this event type in Ganymede source code"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.ObjectEventObjectName); bf.setType(FieldType.STRING); bf.setName("Object Type Name"); bf.setComment("The name of the object that this event is tracking"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.ObjectEventName); bf.setType(FieldType.STRING); bf.setName("Event Name"); bf.setBadChars(":"); bf.setComment("Short name for this event class, suitable for an email message title"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.ObjectEventDescription); bf.setType(FieldType.STRING); bf.setName("Event Description"); bf.setBadChars(":"); bf.setComment("Fuller description for this event class, suitable for an email message body"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.ObjectEventMailToSelf); bf.setType(FieldType.BOOLEAN); bf.setName("Cc Admin"); bf.setComment("If true, mail for this event will always be cc'ed to the admin performing the action"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.ObjectEventMailOwners); bf.setType(FieldType.BOOLEAN); bf.setName("Cc Owner Groups"); bf.setComment("If true, mail for this event will always be cc'ed to the owner groups owning the object"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.ObjectEventExternalMail); bf.setType(FieldType.STRING); bf.setName("External Email"); bf.setArray(true); bf.setComment("Email addresses not stored in Ganymede"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.ObjectEventObjectType); bf.setType(FieldType.NUMERIC); bf.setName("Object Type ID"); bf.setVisibility(false); // we don't want this to be seen by the client bf.setComment("The type code of the object that this event is tracking"); b.addFieldToEnd(bf); // set the label field b.setLabelField(SchemaConstants.ObjectEventLabel); // and link in the base to this DBStore setBase(b); // create user base DBBaseCategory userCategory = new DBBaseCategory(this, "User-Level Objects", rootCategory); rootCategory.addNodeAfter(userCategory, null); b = new DBObjectBase(this, false); b.setName("User"); b.setTypeID(SchemaConstants.UserBase); // 2 userCategory.addNodeAfter(b, null); // add it to the end is ok bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.UserUserName); bf.setType(FieldType.STRING); bf.setName("Username"); bf.setMinLength((short)2); bf.setMaxLength((short)8); bf.setBadChars(" :=><|+[]\\/*;:.,?\""); // See p.252, teach yourself WinNT Server 4 in 14 days bf.setNameSpace("username"); bf.setComment("User name for an individual privileged to log into Ganymede and/or the network"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.UserPassword); bf.setType(FieldType.PASSWORD); bf.setName("Password"); bf.setMaxLength((short)32); bf.setCrypted(true); bf.setComment("Password for an individual privileged to log into Ganymede and/or the network"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.UserAdminPersonae); bf.setType(FieldType.INVID); bf.setTargetBase(SchemaConstants.PersonaBase); bf.setTargetField(SchemaConstants.PersonaAssocUser); bf.setName("Admin Personae"); bf.setArray(true); bf.setComment("A list of admin personae this user can assume"); b.addFieldToEnd(bf); b.setLabelField(SchemaConstants.UserUserName); setBase(b); // create Task Base b = new DBObjectBase(this, false); b.setName("Task"); b.setClassInfo("arlut.csd.ganymede.server.taskCustom", null); b.setTypeID(SchemaConstants.TaskBase); // 5 adminCategory.addNodeAfter(b, null); // add it to the end is ok bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.TaskName); bf.setType(FieldType.STRING); bf.setName("Task Name"); bf.setNameSpace("buildertask"); bf.setComment("Name of this task, as shown in task monitor"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.TaskClass); bf.setType(FieldType.STRING); bf.setName("Task Class"); bf.setBadChars("/:"); bf.setComment("Name of the plug-in class to load on server restart to handle this task"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.TaskRunOnCommit); bf.setType(FieldType.BOOLEAN); bf.setName("Run On Transaction Commit"); bf.setComment("If true, this task will be run on transaction commit"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.TaskRunPeriodically); bf.setType(FieldType.BOOLEAN); bf.setName("Run Periodically"); bf.setComment("If true, this task will be scheduled for periodic execution"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.TaskPeriodUnit); bf.setType(FieldType.STRING); bf.setName("Period Unit"); bf.setComment("What is the unit of time we're using?"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.TaskPeriodCount); bf.setType(FieldType.NUMERIC); bf.setName("Period Count"); bf.setComment("How many time units between task runs?"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.TaskPeriodAnchor); bf.setType(FieldType.DATE); bf.setName("Period Anchor"); bf.setComment("When do we start counting period intervals from?"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.TaskOptionStrings); bf.setType(FieldType.STRING); bf.setArray(true); bf.setName("Option Strings"); bf.setComment("Optional task parameters, interpreted by specific tasks if needed"); b.addFieldToEnd(bf); b.setLabelField(SchemaConstants.TaskName); // and record the base setBase(b); // create Sync Channel Base b = new DBObjectBase(this, false); b.setName("Sync Channel"); b.setClassInfo("arlut.csd.ganymede.server.syncChannelCustom", null); b.setTypeID(SchemaConstants.SyncChannelBase); // 7 adminCategory.addNodeAfter(b, null); // add it to the end is ok bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelName); bf.setType(FieldType.STRING); bf.setName("Name"); bf.setNameSpace("buildertask"); // same namespace as tasks b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelTypeString); bf.setType(FieldType.STRING); bf.setName("Sync Channel Type"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelClassName); bf.setType(FieldType.STRING); bf.setName("Sync Master Classname"); bf.setComment(ts.l("initializeSchema.syncmaster_comment")); // "Added a descriptive comment for the new Sync Master Classname field in the Sync Channel object definition." b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelDirectory); bf.setType(FieldType.STRING); bf.setName("Queue Directory"); bf.setComment(ts.l("initializeSchema.syncqueuedir_comment")); // "Location of the sync channel directory on disk." b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelFullStateFile); bf.setType(FieldType.STRING); bf.setName("Full State File"); bf.setComment(ts.l("initializeSchema.syncfullstatefile_comment")); // "Path to the file to use for full-state XML dumps." b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelServicer); bf.setType(FieldType.STRING); bf.setName("Service Program"); bf.setComment(ts.l("initializeSchema.syncprogram_comment")); // "The location of the program to service this sync channel." b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelFields); bf.setType(FieldType.FIELDOPTIONS); bf.setName("Sync Data"); bf.setComment(ts.l("initializeSchema.syncdata_comment")); // "The definitions for what object and fields we want to include in this sync channel." b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelPlaintextOK); bf.setType(FieldType.BOOLEAN); bf.setName("Allow Plaintext Passwords"); bf.setComment(ts.l("initializeSchema.syncplaintext_comment")); // "Allow Plaintext Passwords to be written to this Sync Channel." b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelTypeNum); bf.setType(FieldType.NUMERIC); bf.setName("Channel Type Index"); b.addFieldToEnd(bf); b.setLabelField(SchemaConstants.SyncChannelName); // and record the base setBase(b); // and lock in the namespaces for (DBNameSpace namespace: nameSpaces) { namespace.schemaEditCommit(); } } catch (RemoteException ex) { throw new RuntimeException("remote :" + ex); } loading = false; } /** * This method is designed to transition from a Ganymede 1.0 * database schema to a Ganymede 2.0 database schema, by adding the * new built-in object and fields that 1.0 lacked. */ void verifySchema2_0() { DBObjectBase b = null; DBObjectBaseField bf = null; /* -- */ try { if (getNameSpace("buildertask") == null) { Ganymede.debug("Adding buildertask name space"); // note! we have had buildertask in the server DBStore // since 1998.. if we're dealing with a ganymede.db file // that doesn't have it, it must be old indeed! DBNameSpace ns = new DBNameSpace("buildertask", true); nameSpaces.add(ns); } if (getObjectBase(SchemaConstants.SyncChannelBase) == null) { Ganymede.debug("Adding Sync Channel ObjectBase"); // create Sync Channel Base b = new DBObjectBase(this, false); b.setName("Sync Channel"); b.setClassInfo("arlut.csd.ganymede.server.syncChannelCustom", null); b.setTypeID(SchemaConstants.SyncChannelBase); // 7 DBBaseCategory adminCategory = (DBBaseCategory) getCategoryNode("/Admin-Level Objects"); adminCategory.addNodeAfter(b, null); // add it to the end is ok bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelName); bf.setType(FieldType.STRING); bf.setName("Name"); bf.setNameSpace("buildertask"); // same namespace as tasks b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelTypeString); bf.setType(FieldType.STRING); bf.setName("Sync Channel Type"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelClassName); bf.setType(FieldType.STRING); bf.setName("Sync Master Classname"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelDirectory); bf.setType(FieldType.STRING); bf.setName("Queue Directory"); bf.setComment("Location of the sync channel directory on disk"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelFullStateFile); bf.setType(FieldType.STRING); bf.setName("Full State File"); bf.setComment("Path to the file to use for full-state XML dumps"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelServicer); bf.setType(FieldType.STRING); bf.setName("Service Program"); bf.setComment("The location of the program to service this sync channel"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelFields); bf.setType(FieldType.FIELDOPTIONS); bf.setName("Sync Data"); bf.setComment("The definitions for what object and fields we want to include in this sync channel"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelPlaintextOK); bf.setType(FieldType.BOOLEAN); bf.setName("Allow Plaintext Passwords"); bf.setComment("Allow Plaintext Passwords to be written to this Sync Channel"); b.addFieldToEnd(bf); bf = new DBObjectBaseField(b); bf.setID(SchemaConstants.SyncChannelTypeNum); bf.setType(FieldType.NUMERIC); bf.setName("Channel Type Index"); b.addFieldToEnd(bf); b.setLabelField(SchemaConstants.SyncChannelName); setBase(b); } else { // we added the SyncChannelTypeString, SyncChannelTypeNum // and SyncChannelFullStateFile fields after releasing a // version without, check to see if we need to add them // note that we redefined the SyncChannel type to have // these fields in DBStore version 2.11. DBObjectBase syncBase = getObjectBase(SchemaConstants.SyncChannelBase); syncBase.setEditingMode(DBObjectBase.EditingMode.LOADING); try { if (syncBase.getField(SchemaConstants.SyncChannelTypeString) == null) { bf = new DBObjectBaseField(syncBase); bf.setID(SchemaConstants.SyncChannelTypeString); bf.setType(FieldType.STRING); bf.setName("Sync Channel Type"); syncBase.addFieldAfter(bf, SchemaConstants.SyncChannelName); } if (syncBase.getField(SchemaConstants.SyncChannelClassName) == null) { bf = new DBObjectBaseField(syncBase); bf.setID(SchemaConstants.SyncChannelClassName); bf.setType(FieldType.STRING); bf.setName("Sync Master Classname"); syncBase.addFieldAfter(bf, SchemaConstants.SyncChannelTypeString); } if (syncBase.getField(SchemaConstants.SyncChannelFullStateFile) == null) { bf = new DBObjectBaseField(syncBase); bf.setID(SchemaConstants.SyncChannelFullStateFile); bf.setType(FieldType.STRING); bf.setName("Full State File"); bf.setComment("Path to the file to use for full-state XML dumps"); syncBase.addFieldAfter(bf, SchemaConstants.SyncChannelDirectory); } if (syncBase.getField(SchemaConstants.SyncChannelTypeNum) == null) { bf = new DBObjectBaseField(syncBase); bf.setID(SchemaConstants.SyncChannelTypeNum); bf.setType(FieldType.NUMERIC); bf.setName("Channel Type Index"); syncBase.addFieldToEnd(bf); } } finally { syncBase.setEditingMode(DBObjectBase.EditingMode.LOCKED); } } // now make sure that the Task base has the TaskOptionStrings field DBObjectBase taskBase = getObjectBase(SchemaConstants.TaskBase); if (taskBase.getField(SchemaConstants.TaskOptionStrings) == null) { Ganymede.debug("Adding TaskOptionStrings to task base"); bf = new DBObjectBaseField(taskBase); bf.setID(SchemaConstants.TaskOptionStrings); bf.setType(FieldType.STRING); bf.setArray(true); bf.setName("Option Strings"); bf.setComment("Optional task parameters, interpreted by specific tasks if needed"); taskBase.addFieldToEnd(bf); } // make sure that the ObjectEvent type has the new hidden label field DBObjectBase objectEventBase = getObjectBase(SchemaConstants.ObjectEventBase); if (objectEventBase.getField(SchemaConstants.ObjectEventLabel) == null) { Ganymede.debug("Adding ObjectEventLabel to ObjectEvent base"); bf = new DBObjectBaseField(objectEventBase); bf.setID(SchemaConstants.ObjectEventLabel); bf.setName("Hidden Label"); bf.setType(FieldType.STRING); bf.setNameSpace("eventtoken"); bf.setVisibility(false); // hidden bf.setComment("Hidden composite label field. The contents of this label field is automatically set from the Event Token and Object Type Name fields."); objectEventBase.addFieldToStart(bf); } objectEventBase.setLabelField(SchemaConstants.ObjectEventLabel); // and this last check is for the old ARL database, which // somehow did not get namespace constrained on the task name // field bf = taskBase.getField(SchemaConstants.TaskName); if (bf.getNameSpace() == null) { Ganymede.debug("Applying namespace constraint to task name"); bf.setNameSpace("buildertask"); } } catch (RemoteException ex) { throw new RuntimeException(ex); } } // debug routine /** * This is a convenience method used by server-side code to send * debug output to stderr and to any attached admin consoles. */ static public void debug(String string) { if (debug) { System.err.println(string); } } /** * This method scans through all of the object bases defined and * verifies that all bases have a designated label field that is * namespace constrained. */ boolean verify_label_fields() { boolean ok = true; for (DBObjectBase base: objectBases.values()) { if (base.getLabelField() == -1) { // "Error, object base {0} has no label field defined." System.err.println(ts.l("verify_label_fields.no_label", base.getName())); ok = false; } else { DBObjectBaseField labelFieldDef = base.getField(base.getLabelField()); if (labelFieldDef.getNameSpace() == null) { // "Error, object base {0}''s label field ({1}) must be namespace-constrained." System.err.println(ts.l("verify_label_fields.no_namespace", base.getName(), labelFieldDef.getName())); ok = false; } } } return ok; } /** * Creates required objects when a new database is created * from scratch, or if a pre-existing but damaged database file * is loaded.. */ void initializeObjects() { DBEditObject eObj; StringDBField s; PasswordDBField p; InvidDBField i; BooleanDBField b; GanymedeSession gSession = null; DBSession session; PermissionMatrixDBField pm; ReturnVal retVal; boolean success = false; Invid supergashOwner = Invid.createInvid(SchemaConstants.OwnerBase, SchemaConstants.OwnerSupergash); Invid supergash = Invid.createInvid(SchemaConstants.PersonaBase, SchemaConstants.PersonaSupergashObj); Invid monitor = Invid.createInvid(SchemaConstants.PersonaBase, SchemaConstants.PersonaMonitorObj); Invid defaultRole = Invid.createInvid(SchemaConstants.RoleBase, SchemaConstants.RoleDefaultObj); /* -- */ // create a 'supergash' session to work with try { gSession = new GanymedeSession(); } catch (RemoteException ex) { throw new Error("RMI system could not initialize GanymedeSession"); } session = gSession.getDBSession(); session.openTransaction("DBStore bootstrap initialization"); try { // make sure the supergash owner group exists if (!exists(session, supergashOwner)) { System.err.println("Creating supergash Owner Group"); retVal = session.createDBObject(SchemaConstants.OwnerBase, supergashOwner, null); if (!ReturnVal.didSucceed(retVal)) { throw new RuntimeException("Couldn't create supergash owner group."); } eObj = (DBEditObject) retVal.getObject(); s = eObj.getStringField("Name"); s.setValueLocal(Ganymede.rootname); } // make sure the supergash admin persona object exists if (!exists(session, supergash)) { System.err.println("Creating supergash persona object"); retVal = session.createDBObject(SchemaConstants.PersonaBase, supergash, null); if (!ReturnVal.didSucceed(retVal)) { throw new RuntimeException("Couldn't create supergash admin persona."); } eObj = (DBEditObject) retVal.getObject(); // set the user visible name s = eObj.getStringField(SchemaConstants.PersonaNameField); s.setValueLocal(Ganymede.rootname); // check to make sure the invisible label field was properly set s = eObj.getStringField(SchemaConstants.PersonaLabelField); if (!s.getValueString().equals(Ganymede.rootname)) { System.err.println("*** Error, supergash label field not automatically set.."); System.err.println("*** problem in adminPersonaCustom? " + s.getValueString()); s.setValueLocal(Ganymede.rootname); } p = eObj.getPassField(SchemaConstants.PersonaPasswordField); p.setPlainTextPass(Ganymede.defaultrootpassProperty); // default supergash password b = eObj.getBooleanField(SchemaConstants.PersonaAdminConsole); b.setValueLocal(Boolean.TRUE); b = eObj.getBooleanField(SchemaConstants.PersonaAdminPower); b.setValueLocal(Boolean.TRUE); } else { eObj = session.editDBObject(supergash); if (eObj == null) { throw new RuntimeException("Couldn't edit supergash admin persona."); } if (Ganymede.rootname != null && !Ganymede.rootname.equals("")) { s = eObj.getStringField(SchemaConstants.PersonaNameField); s.setValueLocal(Ganymede.rootname); } if (Ganymede.resetadmin) { p = eObj.getPassField(SchemaConstants.PersonaPasswordField); p.setPlainTextPass(Ganymede.defaultrootpassProperty); // default supergash password } b = eObj.getBooleanField(SchemaConstants.PersonaAdminConsole); b.setValueLocal(Boolean.TRUE); b = eObj.getBooleanField(SchemaConstants.PersonaAdminPower); b.setValueLocal(Boolean.TRUE); } // make sure the supergash admin persona and supergash owner group are linked. eObj = session.editDBObject(supergash); i = eObj.getInvidField(SchemaConstants.PersonaGroupsField); if (!i.containsElement(supergashOwner)) { System.err.println("Linking supergash Persona object to supergash Owner Group"); i.addElementLocal(supergashOwner); } // make sure the monitor object exists if the properties file defines one if (!exists(session, monitor) && Ganymede.monitornameProperty != null && Ganymede.defaultmonitorpassProperty != null) { System.err.println("Creating monitor persona"); retVal = session.createDBObject(SchemaConstants.PersonaBase, monitor, null); if (!ReturnVal.didSucceed(retVal)) { throw new RuntimeException("Couldn't create monitor admin persona."); } if (Ganymede.monitornameProperty == null) { throw new NullPointerException("monitor name property not loaded, can't initialize monitor account"); } eObj = (DBEditObject) retVal.getObject(); s = eObj.getStringField(SchemaConstants.PersonaNameField); s.setValueLocal(Ganymede.monitornameProperty); // check our autonomic functioning s = eObj.getStringField(SchemaConstants.PersonaLabelField); if (!s.getValueString().equals(Ganymede.monitornameProperty)) { System.err.println("*** Error, monitor label field not automatically set.."); System.err.println("*** problem in adminPersonaCustom? " + s.getValueString()); s.setValueLocal(Ganymede.monitornameProperty); } // s.setValueLocal(Ganymede.monitornameProperty); p = eObj.getPassField(SchemaConstants.PersonaPasswordField); if (Ganymede.defaultmonitorpassProperty != null) { p.setPlainTextPass(Ganymede.defaultmonitorpassProperty); // default monitor password } else { throw new NullPointerException("monitor password property not loaded, can't initialize monitor account"); } b = eObj.getBooleanField(SchemaConstants.PersonaAdminConsole); b.setValueLocal(Boolean.TRUE); b = eObj.getBooleanField(SchemaConstants.PersonaAdminPower); b.setValueLocal(Boolean.FALSE); } // make sure we have the default role object if (!exists(session, defaultRole)) { System.err.println("Creating default Role object"); retVal = session.createDBObject(SchemaConstants.RoleBase, defaultRole, null); if (!ReturnVal.didSucceed(retVal)) { throw new RuntimeException("Couldn't create permissions default object."); } eObj = (DBEditObject) retVal.getObject(); s = eObj.getStringField(SchemaConstants.RoleName); s.setValueLocal("Default"); // what can users do with objects they own? Includes users themselves pm = eObj.getPermField(SchemaConstants.RoleMatrix); // view self, nothing else pm.setPerm(SchemaConstants.UserBase, PermEntry.getPermEntry(true, false, false, false)); } createSysEventObj(session, "abnormallogout", "Unusual Logout", null, false); createSysEventObj(session, "adminconnect", "Admin Console Attached", "Admin Console Attached", false); createSysEventObj(session, "admindisconnect", "Admin Console Disconnected", "Admin Console Disconnected", false); createSysEventObj(session, "badpass", "Failed login attempt", "Bad username and/or password", true); createSysEventObj(session, "deleteobject", "Object Deleted", "This object has been deleted.", true); createSysEventObj(session, "dump", "Database Dump", "Database Dump", false); createSysEventObj(session, "expirationwarn", "Expiration Warning", "This object is going to expire soon.", false); createSysEventObj(session, "expirenotify", "Expiration Notification", "This object has been expired.", false); createSysEventObj(session, "externalerror", "Error Running External Process", "The Ganymede Server encountered an error when attempting to run an external process.", false); createSysEventObj(session, "finishtransaction", "transaction end", null, false); createSysEventObj(session, "goodlogin", "Successful login", null, false); createSysEventObj(session, "inactivateobject", "Object Inactivation", "This object has been inactivated", true); createSysEventObj(session, "journalreset", "Journal File Reset", "Journal file reset", false); createSysEventObj(session, "normallogout", "Normal Logout", null, false); createSysEventObj(session, "objectchanged", "Object Changed", "Object Changed", true); createSysEventObj(session, "objectcreated", "Object Created", "Object Created", true); createSysEventObj(session, "reactivateobject", "Object Reactivation", "This object has been reactivated", true); createSysEventObj(session, "removalwarn", "Removal Warning", "This object is going to be removed", false); createSysEventObj(session, "removenotify", "Removal Notification", "This object has been removed", false); createSysEventObj(session, "restart", "Server Restarted", "The Ganymede server was restarted", false); createSysEventObj(session, "shutdown", "Server shutdown", "The Ganymede server was cleanly shut down", false); // Create the start transaction oBject. This object is // consulted by the DBLog class to guide how mail should be // handled for transaction logs. DBEditObject transactionEvent = createSysEventObj(session, "starttransaction", "transaction start", null, false); if (transactionEvent != null) { transactionEvent.setFieldValueLocal(SchemaConstants.EventMailBoolean, Boolean.TRUE); transactionEvent.setFieldValueLocal(SchemaConstants.EventMailToSelf, Boolean.TRUE); transactionEvent.setFieldValueLocal(SchemaConstants.EventMailOwners, Boolean.TRUE); transactionEvent.setFieldValueLocal(SchemaConstants.NotesField, "This system event object is consulted to determine whether " + "mail should be sent\nout when a transaction is committed.\n\n" + "If the 'Event Mail' checkbox is set to false, no transaction log " + "mail will be sent\nfrom the Ganymede server, ever.\n\nIf the " + "'Cc: Admin' checkbox is set to true, the admin who committed the " + "transaction will receive a copy of the transaction log.\n\n" + "If the 'Cc: Owner Groups' checkbox is set to true, the members of " + "the owner groups whose objects were affected by the transaction " + "committed will each receive a copy of that portion of the transaction " + "log that concerns objects owned by those admins."); } // we commit the transaction at the DBStore level because we don't // want to mess with running builder tasks retVal = session.commitTransaction(); // if the DBSession commit failed, we won't get an automatic // abort.. do that here. try { if (!ReturnVal.didSucceed(retVal)) { try { session.abortTransaction(); } catch (Throwable ex) { Ganymede.logError(ex); } finally { success=true; // true enough, anyway } } } finally { gSession.logout(); } success = true; } catch (Throwable ex) { Ganymede.logError(ex); } finally { if (!success) { session.abortTransaction(); gSession.logout(); } } } /** * Convenience method for initializeObjects(). */ private boolean exists(DBSession session, Invid invid) { return (session.viewDBObject(invid) != null); } /** * Convenience method for initializeObjects(). */ private DBEditObject createSysEventObj(DBSession session, String token, String name, String description, boolean ccAdmin) { DBEditObject eO = null; ReturnVal retVal; try { if (session.getGSession().findLabeledObject(token, SchemaConstants.EventBase) != null) { return null; } } catch (NotLoggedInException ex) { throw new Error("Mysterious not logged in exception: " + ex.getMessage()); } System.err.println("Creating " + token + " system event object"); retVal = session.createDBObject(SchemaConstants.EventBase, null); if (!ReturnVal.didSucceed(retVal)) { throw new RuntimeException("Error, could not create system event object " + token); } eO = (DBEditObject) retVal.getObject(); retVal = eO.setFieldValueLocal(SchemaConstants.EventToken, token); if (!ReturnVal.didSucceed(retVal)) { throw new RuntimeException("Error, could not set token for system event object " + token); } retVal = eO.setFieldValueLocal(SchemaConstants.EventName, name); if (!ReturnVal.didSucceed(retVal)) { throw new RuntimeException("Error, could not set name for system event object " + token); } if (description != null) { retVal = eO.setFieldValueLocal(SchemaConstants.EventDescription, description); if (!ReturnVal.didSucceed(retVal)) { throw new RuntimeException("Error, could not set description system event object " + token); } } return eO; } /* -- The following methods are used to keep track of DBStore state and are reflected in updates to any connected Admin consoles. These methods are for statistics keeping only. -- */ /** * Increments the count of checked-out objects for the admin consoles. */ void checkOut() { objectsCheckedOut++; GanymedeAdmin.updateCheckedOut(); } /** * Decrements the count of checked-out objects for the admin consoles. */ void checkIn() { objectsCheckedOut--; GanymedeAdmin.updateCheckedOut(); if (objectsCheckedOut < 0) { throw new RuntimeException("Objects checked out has gone negative"); } } /* ************************************************************************* * * The following methods are for Jython/Map support * * For this object, the Map interface allows for indexing based on either * the name or the type ID of a DBObjectBase. Indexing by type id, however, * is only supported for "direct" access to the Map; the type id numbers * won't appear in the list of keys for the Map. * * EXAMPLE: * MyDBStoreObject.get("Users") will return the DBObjectBase with the label * of "Users". * */ public boolean containsKey(Object key) { return keySet().contains(key); } public boolean has_key(Object key) { return containsKey(key); } public boolean containsValue(Object value) { return getBases().contains(value); } public Set<Entry> entrySet() { Set<Entry> entrySet = new HashSet<Entry>(); synchronized( objectBases) { for (DBObjectBase base: objectBases.values()) { entrySet.add(new Entry(base)); } } return entrySet; } public Object get(Object key) { if (key instanceof Short) { return getObjectBase((Short) key); } else if (key instanceof String) { return getObjectBase((String) key); } else { return null; } } public Set<String> keySet() { return new HashSet<String>(getBaseNameList()); } public Set<String> keys() { return keySet(); } public List items() { List list = new ArrayList(); Object[] tuple; for (DBObjectBase base: objectBases.values()) { tuple = new Object[2]; tuple[0] = base.getName(); tuple[1] = base; list.add(tuple); } return list; } public int size() { return getBaseNameList().size(); } public Collection<DBObjectBase> values() { return new ArrayList(getBases()); } public String toString() { return keySet().toString(); } /** * Implements key/value pairs for use in a * {@link java.util.Map}'s {@link java.util.Map#entrySet()} method. */ static class Entry implements Map.Entry { Object key, value; public Entry( DBObjectBase base ) { key = base.getName(); value = base; } public Object getKey() { return key; } public Object getValue() { return value; } public Object setValue(Object value) { return null; } } /* * These methods are are no-ops since we don't want this object * messed with via the Map interface. */ public Object put(Object key, Object value) { return null; } public void putAll(Map t) { return; } public Object remove(Object key) { return null; } public void clear() { return; } public boolean isEmpty() { return getBaseNameList().isEmpty(); } }