/* The contents of this file are subject to the license and copyright terms * detailed in the license directory at the root of the source tree (also * available online at http://fedora-commons.org/license/). */ package fedora.server.storage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; import org.apache.log4j.Logger; import fedora.common.Constants; import fedora.common.Models; import fedora.server.Context; import fedora.server.Module; import fedora.server.RecoveryContext; import fedora.server.Server; import fedora.server.errors.ConnectionPoolNotFoundException; import fedora.server.errors.GeneralException; import fedora.server.errors.InvalidContextException; import fedora.server.errors.LowlevelStorageException; import fedora.server.errors.ModuleInitializationException; import fedora.server.errors.ObjectAlreadyInLowlevelStorageException; import fedora.server.errors.ObjectExistsException; import fedora.server.errors.ObjectLockedException; import fedora.server.errors.ObjectNotFoundException; import fedora.server.errors.ObjectNotInLowlevelStorageException; import fedora.server.errors.ServerException; import fedora.server.errors.StorageDeviceException; import fedora.server.errors.StreamIOException; import fedora.server.management.Management; import fedora.server.management.PIDGenerator; import fedora.server.resourceIndex.ResourceIndex; import fedora.server.search.FieldSearch; import fedora.server.search.FieldSearchQuery; import fedora.server.search.FieldSearchResult; import fedora.server.storage.lowlevel.ILowlevelStorage; import fedora.server.storage.translation.DOTranslationUtility; import fedora.server.storage.translation.DOTranslator; import fedora.server.storage.types.BasicDigitalObject; import fedora.server.storage.types.Datastream; import fedora.server.storage.types.DatastreamManagedContent; import fedora.server.storage.types.DatastreamXMLMetadata; import fedora.server.storage.types.DigitalObject; import fedora.server.storage.types.DigitalObjectUtil; import fedora.server.storage.types.MIMETypedStream; import fedora.server.storage.types.RelationshipTuple; import fedora.server.utilities.DCField; import fedora.server.utilities.DCFields; import fedora.server.utilities.SQLUtility; import fedora.server.utilities.StreamUtility; import fedora.server.validation.DOValidator; import fedora.server.validation.DOValidatorImpl; import fedora.server.validation.ValidationUtility; /** * Manages the reading and writing of digital objects by instantiating an * appropriate object reader or writer. Also, manages the object ingest process * and the object replication process. * * @author Chris Wilper * @version $Id$ */ public class DefaultDOManager extends Module implements DOManager { /** Logger for this class. */ private static final Logger LOG = Logger.getLogger(DefaultDOManager.class.getName()); private static final Pattern URL_PROTOCOL = Pattern.compile("^\\w+:\\/.*$"); private String m_pidNamespace; protected String m_storagePool; private String m_defaultStorageFormat; private String m_defaultExportFormat; private String m_storageCharacterEncoding; protected PIDGenerator m_pidGenerator; protected DOTranslator m_translator; protected ILowlevelStorage m_permanentStore; protected DOValidator m_validator; protected FieldSearch m_fieldSearch; protected ExternalContentManager m_contentManager; protected Management m_management; protected Set<String> m_retainPIDs; protected ResourceIndex m_resourceIndex; private DOReaderCache m_readerCache; private final Set<String> m_lockedPIDs; protected ConnectionPool m_connectionPool; protected Connection m_connection; private ModelDeploymentMap m_cModelDeploymentMap; /** * Creates a new DefaultDOManager. */ public DefaultDOManager(Map<String, String> moduleParameters, Server server, String role) throws ModuleInitializationException { super(moduleParameters, server, role); m_lockedPIDs = new HashSet<String>(); } /** * Gets initial param values. */ @Override public void initModule() throws ModuleInitializationException { // pidNamespace (required, 1-17 chars, a-z, A-Z, 0-9 '-' '.') m_pidNamespace = getParameter("pidNamespace"); if (m_pidNamespace == null) { throw new ModuleInitializationException("pidNamespace parameter must be specified.", getRole()); } if (m_pidNamespace.length() > 17 || m_pidNamespace.length() < 1) { throw new ModuleInitializationException("pidNamespace parameter must be 1-17 chars long", getRole()); } StringBuffer badChars = new StringBuffer(); for (int i = 0; i < m_pidNamespace.length(); i++) { char c = m_pidNamespace.charAt(i); boolean invalid = true; if (c >= '0' && c <= '9') { invalid = false; } else if (c >= 'a' && c <= 'z') { invalid = false; } else if (c >= 'A' && c <= 'Z') { invalid = false; } else if (c == '-') { invalid = false; } else if (c == '.') { invalid = false; } if (invalid) { badChars.append(c); } } if (badChars.toString().length() > 0) { throw new ModuleInitializationException("pidNamespace contains " + "invalid character(s) '" + badChars .toString() + "'", getRole()); } // storagePool (optional, default=ConnectionPoolManager's default pool) m_storagePool = getParameter("storagePool"); if (m_storagePool == null) { LOG.debug("Parameter storagePool " + "not given, will defer to ConnectionPoolManager's " + "default pool."); } // internal storage format (required) LOG.debug("Server property format.storage= " + Server.STORAGE_FORMAT); m_defaultStorageFormat = Server.STORAGE_FORMAT; if (m_defaultStorageFormat == null) { throw new ModuleInitializationException("System property format.storage " + "not given, but it's required.", getRole()); } // default export format (required) m_defaultExportFormat = getParameter("defaultExportFormat"); if (m_defaultExportFormat == null) { throw new ModuleInitializationException("Parameter defaultExportFormat " + "not given, but it's required.", getRole()); } // storageCharacterEncoding (optional, default=UTF-8) m_storageCharacterEncoding = getParameter("storageCharacterEncoding"); if (m_storageCharacterEncoding == null) { LOG.debug("Parameter storage_character_encoding " + "not given, using UTF-8"); m_storageCharacterEncoding = "UTF-8"; } initRetainPID(); // readerCacheSize and readerCacheSeconds (optional, defaults = 20, 5) String rcSize = getParameter("readerCacheSize"); if (rcSize == null) { LOG.debug("Parameter readerCacheSize not given, using 20"); rcSize = "20"; } int readerCacheSize; try { readerCacheSize = Integer.parseInt(rcSize); if (readerCacheSize < 0) { throw new Exception("Cannot be less than zero"); } } catch (Exception e) { throw new ModuleInitializationException("Bad value for readerCacheSize parameter: " + e.getMessage(), getRole()); } String rcSeconds = getParameter("readerCacheSeconds"); if (rcSeconds == null) { LOG.debug("Parameter readerCacheSeconds not given, using 5"); rcSeconds = "5"; } int readerCacheSeconds; try { readerCacheSeconds = Integer.parseInt(rcSeconds); if (readerCacheSeconds < 1) { throw new Exception("Cannot be less than one"); } } catch (Exception e) { throw new ModuleInitializationException("Bad value for readerCacheSeconds parameter: " + e.getMessage(), getRole()); } if (readerCacheSize > 0) { m_readerCache = new DOReaderCache(readerCacheSize, readerCacheSeconds); } } protected void initRetainPID() { m_retainPIDs = new HashSet<String>(); String retainPIDs = getParameter("retainPIDs"); if (retainPIDs == null || retainPIDs.equals("*")) { // when m_retainPIDS is set to null, that means "all" m_retainPIDs = null; } else { // add to list (accept space and/or comma-separated) String[] ns = retainPIDs.trim().replaceAll(" +", ",") .replaceAll(",+", ",").split(","); for (String element : ns) { if (element.length() > 0) { m_retainPIDs.add(element); } } // fedora-system PIDs must be ingestable as-is m_retainPIDs.add("fedora-system"); } } @Override public void postInitModule() throws ModuleInitializationException { // get ref to management module m_management = (Management) getServer() .getModule("fedora.server.management.Management"); if (m_management == null) { throw new ModuleInitializationException("Management module not loaded.", getRole()); } // get ref to contentmanager module m_contentManager = (ExternalContentManager) getServer() .getModule("fedora.server.storage.ExternalContentManager"); if (m_contentManager == null) { throw new ModuleInitializationException("ExternalContentManager not loaded.", getRole()); } // get ref to fieldsearch module m_fieldSearch = (FieldSearch) getServer() .getModule("fedora.server.search.FieldSearch"); // get ref to pidgenerator m_pidGenerator = (PIDGenerator) getServer() .getModule("fedora.server.management.PIDGenerator"); // note: permanent and temporary storage handles are lazily instantiated // get ref to translator and derive storageFormat default if not given m_translator = (DOTranslator) getServer() .getModule("fedora.server.storage.translation.DOTranslator"); // get ref to digital object validator m_validator = (DOValidator) getServer() .getModule("fedora.server.validation.DOValidator"); if (m_validator == null) { throw new ModuleInitializationException("DOValidator not loaded.", getRole()); } // get ref to ResourceIndex m_resourceIndex = (ResourceIndex) getServer() .getModule("fedora.server.resourceIndex.ResourceIndex"); if (m_resourceIndex == null) { LOG.error("ResourceIndex not loaded"); throw new ModuleInitializationException("ResourceIndex not loaded", getRole()); } // now get the connectionpool ConnectionPoolManager cpm = (ConnectionPoolManager) getServer() .getModule("fedora.server.storage.ConnectionPoolManager"); if (cpm == null) { throw new ModuleInitializationException("ConnectionPoolManager not loaded.", getRole()); } try { if (m_storagePool == null) { m_connectionPool = cpm.getPool(); } else { m_connectionPool = cpm.getPool(m_storagePool); } } catch (ConnectionPoolNotFoundException cpnfe) { throw new ModuleInitializationException("Couldn't get required " + "connection pool; wasn't found", getRole()); } try { String dbSpec = "fedora/server/storage/resources/DefaultDOManager.dbspec"; InputStream specIn = this.getClass().getClassLoader() .getResourceAsStream(dbSpec); if (specIn == null) { throw new IOException("Cannot find required " + "resource: " + dbSpec); } SQLUtility.createNonExistingTables(m_connectionPool, specIn); } catch (Exception e) { throw new ModuleInitializationException("Error while attempting to " + "check for and create non-existing table(s): " + e.getClass() .getName() + ": " + e.getMessage(), getRole()); } // get ref to lowlevelstorage module m_permanentStore = (ILowlevelStorage) getServer() .getModule("fedora.server.storage.lowlevel.ILowlevelStorage"); if (m_permanentStore == null) { LOG.error("LowlevelStorage not loaded"); throw new ModuleInitializationException("LowlevelStorage not loaded", getRole()); } /* Load the service deployment cache from the registry */ initializeCModelDeploymentCache(); } public String lookupDeploymentForCModel(String cModelPid, String sDefPid) { return m_cModelDeploymentMap.getDeployment(ServiceContext .getInstance(cModelPid, sDefPid)); } private void initializeCModelDeploymentCache() { // Initialize Map containing links from Content Models to the Service Deployments. m_cModelDeploymentMap = new ModelDeploymentMap(); LOG.debug("Initializing content model deployment map"); Connection c = null; Statement s = null; ResultSet r = null; try { c = m_connectionPool.getConnection(); s = c.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); ResultSet results = s.executeQuery("SELECT cModel, sDef, sDep, mDate " + " FROM modelDeploymentMap, doFields " + " WHERE doFields.pid = modelDeploymentMap.sDep"); while (results.next()) { String cModel = results.getString(1); String sDef = results.getString(2); String sDep = results.getString(3); long lastMod = results.getLong(4); m_cModelDeploymentMap.putDeployment(ServiceContext .getInstance(cModel, sDef), sDep, lastMod); } } catch (SQLException e) { throw new RuntimeException("Error loading cModel deployment cach", e); } finally { try { if (r != null) { r.close(); } if (s != null) { s.close(); } if (c != null) { m_connectionPool.free(c); } } catch (SQLException e) { throw new RuntimeException("Error loading cModel deployment cach", e); } } } /** * Update the registry and deployment cache to reflect the latest state of * reality. * * @param obj * DOReader of a service deployment object */ private synchronized void updateDeploymentMap(DigitalObject obj, Connection c, boolean isPurge) throws SQLException { String sDep = obj.getPid(); Set<RelationshipTuple> sDefs = obj.getRelationships(Constants.MODEL.IS_DEPLOYMENT_OF, null); Set<RelationshipTuple> models = obj.getRelationships(Constants.MODEL.IS_CONTRACTOR_OF, null); /* Read in the new deployment map from the object */ Set<ServiceContext> newContext = new HashSet<ServiceContext>(); if (!isPurge) { for (RelationshipTuple sDefTuple : sDefs) { String sDef = sDefTuple.getObjectPID(); for (RelationshipTuple cModelTuple : models) { String cModel = cModelTuple.getObjectPID(); newContext.add(ServiceContext.getInstance(cModel, sDef)); } } } /* Read in the old deployment map from the cache */ Set<ServiceContext> oldContext = m_cModelDeploymentMap.getContextFor(sDep); /* Remove any obsolete deployments from the registry/cache */ for (ServiceContext o : oldContext) { if (!newContext.contains(o)) { removeDeployment(o, obj, c); } } /* Add any new deployments from the registry/cache */ for (ServiceContext n : newContext) { if (!oldContext.contains(n)) { addDeployment(n, obj, c); } else { updateDeployment(n, obj, c); } } } private void addDeployment(ServiceContext context, DigitalObject sDep, Connection c) throws SQLException { Statement s = c.createStatement(); try { s .executeUpdate("INSERT INTO modelDeploymentMap (cModel, sDef, sDep) VALUES ('" + context.cModel + "' , '" + context.sDef + "', '" + sDep.getPid() + "')"); } finally { if (s != null) { s.close(); } } m_cModelDeploymentMap.putDeployment(context, sDep.getPid(), sDep .getLastModDate().getTime()); } private void updateDeployment(ServiceContext context, DigitalObject sDep, Connection c) throws SQLException { m_cModelDeploymentMap.putDeployment(context, sDep.getPid(), sDep .getLastModDate().getTime()); } private void removeDeployment(ServiceContext context, DigitalObject sDep, Connection c) throws SQLException { Statement s = c.createStatement(); try { s.executeUpdate("DELETE FROM modelDeploymentMap " + "WHERE cModel = '" + context.cModel + "' AND sDef ='" + context.sDef + "' AND sDep = '" + sDep.getPid() + "'"); } finally { if (s != null) { s.close(); } } m_cModelDeploymentMap.removeDeployment(context, sDep.getPid()); } @Override public void shutdownModule() { if (m_readerCache != null) { m_readerCache.close(); } } public void releaseWriter(DOWriter writer) { // If this is a new object, but object was not successfully committed // need to backout object registration. if (writer.isNew() && !writer.isCommitted()) { try { unregisterObject(writer.getObject()); } catch (Exception e) { try { LOG.warn("Error unregistering object: " + writer.GetObjectPID()); } catch (Exception e2) { LOG .warn("Error unregistering object; Unable to obtain PID from writer."); } } } writer.invalidate(); try { releaseWriteLock(writer.GetObjectPID()); } catch (ServerException e) { LOG .warn("Error releasing object lock; Unable to obtain pid from writer."); } } private void releaseWriteLock(String pid) { synchronized (m_lockedPIDs) { m_lockedPIDs.remove(pid); } } private void getWriteLock(String pid) throws ObjectLockedException { synchronized (m_lockedPIDs) { if (m_lockedPIDs.contains(pid)) { throw new ObjectLockedException(pid + " is currently being " + "modified by another thread"); } else { m_lockedPIDs.add(pid); } } } public ConnectionPool getConnectionPool() { return m_connectionPool; } public DOValidator getDOValidator() { return m_validator; } @Override public String[] getRequiredModuleRoles() { return new String[] {"fedora.server.management.PIDGenerator", "fedora.server.search.FieldSearch", "fedora.server.storage.ConnectionPoolManager", "fedora.server.storage.lowlevel.ILowlevelStorage", "fedora.server.storage.ExternalContentManager", "fedora.server.storage.translation.DOTranslator", "fedora.server.validation.DOValidator"}; } public String getStorageFormat() { return m_defaultStorageFormat; } public String getDefaultExportFormat() { return m_defaultExportFormat; } public String getStorageCharacterEncoding() { return m_storageCharacterEncoding; } public DOTranslator getTranslator() { return m_translator; } /** * Gets a reader on an an existing digital object. */ public DOReader getReader(boolean cachedObjectRequired, Context context, String pid) throws ServerException { long getReaderStartTime = System.currentTimeMillis(); String source = null; try { { DOReader reader = null; if (m_readerCache != null) { reader = m_readerCache.get(pid); } if (reader == null) { reader = new SimpleDOReader(context, this, m_translator, m_defaultExportFormat, m_defaultStorageFormat, m_storageCharacterEncoding, m_permanentStore .retrieveObject(pid)); source = "filesystem"; if (m_readerCache != null) { m_readerCache.put(reader); } } else { source = "memory"; } return reader; } } finally { if (LOG.isDebugEnabled()) { long dur = System.currentTimeMillis() - getReaderStartTime; LOG.debug("Got DOReader (source=" + source + ") for " + pid + " in " + dur + "ms."); } } } /** * Gets a reader on an an existing service deployment object. */ public ServiceDeploymentReader getServiceDeploymentReader(boolean cachedObjectRequired, Context context, String pid) throws ServerException { { return new SimpleServiceDeploymentReader(context, this, m_translator, m_defaultExportFormat, m_defaultStorageFormat, m_storageCharacterEncoding, m_permanentStore .retrieveObject(pid)); } } /** * Gets a reader on an an existing service definition object. */ public ServiceDefinitionReader getServiceDefinitionReader(boolean cachedObjectRequired, Context context, String pid) throws ServerException { { return new SimpleServiceDefinitionReader(context, this, m_translator, m_defaultExportFormat, m_defaultStorageFormat, m_storageCharacterEncoding, m_permanentStore .retrieveObject(pid)); } } /** * Gets a writer on an an existing object. */ public DOWriter getWriter(boolean cachedObjectRequired, Context context, String pid) throws ServerException, ObjectLockedException { if (cachedObjectRequired) { throw new InvalidContextException("A DOWriter is unavailable in a cached context."); } else { BasicDigitalObject obj = new BasicDigitalObject(); m_translator.deserialize(m_permanentStore.retrieveObject(pid), obj, m_defaultStorageFormat, m_storageCharacterEncoding, DOTranslationUtility.DESERIALIZE_INSTANCE); DOWriter w = new SimpleDOWriter(context, this, m_translator, m_defaultStorageFormat, m_storageCharacterEncoding, obj); getWriteLock(obj.getPid()); return w; } } /** * Manages the INGEST process which includes validation of the ingest XML * file, deserialization of the XML into a Digital Object instance, setting * of properties on the object by the system (dates and states), PID * validation or generation, object registry functions, getting a writer for * the digital object, and ultimately writing the object to persistent * storage via the writer. * * @param context * @param in * the input stream that is the XML ingest file for a digital object * @param format * the format of the XML ingest file (e.g., FOXML, Fedora METS) * @param encoding * the character encoding of the XML ingest file (e.g., UTF-8) * @param newPid * true if the system should generate a new PID for the object */ public synchronized DOWriter getIngestWriter(boolean cachedObjectRequired, Context context, InputStream in, String format, String encoding, boolean newPid) throws ServerException { LOG.debug("Entered getIngestWriter"); DOWriter w = null; BasicDigitalObject obj = null; File tempFile = null; if (cachedObjectRequired) { throw new InvalidContextException("A DOWriter is unavailable in a cached context."); } else { try { // CURRENT TIME: // Get the current time to use for created dates on object // and object components (if they are not already there). Date nowUTC = Server.getCurrentDate(context); // TEMP STORAGE: // write ingest input stream to a temporary file tempFile = File.createTempFile("fedora-ingest-temp", ".xml"); LOG.debug("Creating temporary file for ingest: " + tempFile.toString()); StreamUtility.pipeStream(in, new FileOutputStream(tempFile), 4096); // VALIDATION: // perform initial validation of the ingest submission file LOG.debug("Validation (ingest phase)"); m_validator.validate(tempFile, format, DOValidatorImpl.VALIDATE_ALL, "ingest"); // DESERIALIZE: // deserialize the ingest input stream into a digital object instance obj = new BasicDigitalObject(); obj.setNew(true); LOG.debug("Deserializing from format: " + format); m_translator .deserialize(new FileInputStream(tempFile), obj, format, encoding, DOTranslationUtility.DESERIALIZE_INSTANCE); // SET OBJECT PROPERTIES: LOG.debug("Setting object/component states and create dates if unset"); // set object state to "A" (Active) if not already set if (obj.getState() == null || obj.getState().equals("")) { obj.setState("A"); } // set object create date to UTC if not already set if (obj.getCreateDate() == null || obj.getCreateDate().equals("")) { obj.setCreateDate(nowUTC); } // set object last modified date to UTC obj.setLastModDate(nowUTC); // SET DATASTREAM PROPERTIES... Iterator<String> dsIter = obj.datastreamIdIterator(); while (dsIter.hasNext()) { for (Datastream ds : obj.datastreams(dsIter.next())) { // Set create date to UTC if not already set if (ds.DSCreateDT == null || ds.DSCreateDT.equals("")) { ds.DSCreateDT = nowUTC; } // Set state to "A" (Active) if not already set if (ds.DSState == null || ds.DSState.equals("")) { ds.DSState = "A"; } ds.DSChecksumType = Datastream .validateChecksumType(ds.DSChecksumType); } } // SET MIMETYPE AND FORMAT_URIS FOR LEGACY OBJECTS' DATASTREAMS if (FOXML1_0.uri.equals(format) || FOXML1_0_LEGACY.equals(format) || METS_EXT1_0.uri.equals(format) || METS_EXT1_0_LEGACY.equals(format)) { DigitalObjectUtil.updateLegacyDatastreams(obj); } // PID VALIDATION: // validate and normalized the provided pid, if any if (obj.getPid() != null && obj.getPid().length() > 0) { obj.setPid(Server.getPID(obj.getPid()).toString()); } // PID GENERATION: // have the system generate a PID if one was not provided if (obj.getPid() != null && obj.getPid().indexOf(":") != -1 && (m_retainPIDs == null || m_retainPIDs.contains(obj .getPid().split(":")[0]))) { LOG .debug("Stream contained PID with retainable namespace-id; will use PID from stream"); try { m_pidGenerator.neverGeneratePID(obj.getPid()); } catch (IOException e) { throw new GeneralException("Error calling pidGenerator.neverGeneratePID(): " + e.getMessage()); } } else { if (newPid) { LOG.debug("Client wants a new PID"); // yes... so do that, then set it in the obj. String p = null; try { // If the context contains a recovery PID, use that. // Otherwise, generate a new PID as usual. if (context instanceof RecoveryContext) { RecoveryContext rContext = (RecoveryContext) context; p = rContext .getRecoveryValue(Constants.RECOVERY.PID.uri); } if (p == null) { p = m_pidGenerator .generatePID(m_pidNamespace) .toString(); } else { LOG .debug("Using new PID from recovery context"); m_pidGenerator.neverGeneratePID(p); } } catch (Exception e) { throw new GeneralException("Error generating PID, PIDGenerator returned unexpected error: (" + e.getClass().getName() + ") - " + e.getMessage()); } LOG.info("Generated new PID: " + p); obj.setPid(p); } else { LOG.debug("Client wants to use existing PID."); } } LOG.info("New object PID is " + obj.getPid()); // CHECK REGISTRY: // ensure the object doesn't already exist if (objectExists(obj.getPid())) { throw new ObjectExistsException("The PID '" + obj.getPid() + "' already exists in the registry; the object can't be re-created."); } // GET DIGITAL OBJECT WRITER: // get an object writer configured with the DEFAULT export format LOG.debug("Getting new writer with default export format: " + m_defaultExportFormat); LOG.debug("Instantiating a SimpleDOWriter"); w = new SimpleDOWriter(context, this, m_translator, m_defaultExportFormat, m_storageCharacterEncoding, obj); // WRITE LOCK: // ensure no one else can modify the object now getWriteLock(obj.getPid()); // DEFAULT DATASTREAMS: populateDC(obj, w, nowUTC); // DATASTREAM VALIDATION ValidationUtility.validateReservedDatastreams(w); // REGISTRY: // at this point the object is valid, so make a record // of it in the digital object registry registerObject(obj, getUserId(context), obj.getLabel(), obj .getCreateDate(), obj.getLastModDate()); return w; } catch (IOException e) { if (w != null) { releaseWriteLock(obj.getPid()); } throw new GeneralException("Error reading/writing temporary " + "ingest file", e); } catch (Exception e) { if (w != null) { releaseWriteLock(obj.getPid()); } if (e instanceof ServerException) { ServerException se = (ServerException) e; throw se; } throw new GeneralException("Ingest failed: " + e.getClass().getName(), e); } finally { if (tempFile != null) { LOG.debug("Finally, removing temp file"); try { tempFile.delete(); } catch (Exception e) { // don't worry if it doesn't exist } } } } } /** * Adds a minimal DC datastream if one isn't already present. * * If there is already a DC datastream, ensure one of the * dc:identifier values is the PID of the object. */ private static void populateDC(DigitalObject obj, DOWriter w, Date nowUTC) throws IOException, ServerException { LOG.debug("Adding/Checking default DC datastream"); DatastreamXMLMetadata dc = (DatastreamXMLMetadata) w.GetDatastream("DC", null); DCFields dcf; if (dc == null) { dc = new DatastreamXMLMetadata("UTF-8"); dc.DSMDClass = 0; //dc.DSMDClass=DatastreamXMLMetadata.DESCRIPTIVE; dc.DatastreamID = "DC"; dc.DSVersionID = "DC1.0"; dc.DSControlGrp = "X"; dc.DSCreateDT = nowUTC; dc.DSLabel = "Dublin Core Record for this object"; dc.DSMIME = "text/xml"; dc.DSFormatURI = OAI_DC2_0.uri; dc.DSSize = 0; dc.DSState = "A"; dc.DSVersionable = true; dcf = new DCFields(); if (obj.getLabel() != null && !obj.getLabel().equals("")) { dcf.titles().add(new DCField(obj.getLabel())); } w.addDatastream(dc, dc.DSVersionable); } else { dcf = new DCFields(new ByteArrayInputStream(dc.xmlContent)); } // ensure one of the dc:identifiers is the pid boolean sawPid = false; for (DCField dcField : dcf.identifiers()) { if (dcField.getValue().equals(obj.getPid())) { sawPid = true; } } if (!sawPid) { dcf.identifiers().add(new DCField(obj.getPid())); } // set the value of the dc datastream according to what's in the DCFields object try { dc.xmlContent = dcf.getAsXML().getBytes("UTF-8"); } catch (UnsupportedEncodingException uee) { // safely ignore... we know UTF-8 works } } /** * The doCommit method finalizes an ingest/update/remove of a digital * object. The process makes updates the object modified date, stores * managed content datastreams, creates the final XML serialization of the * digital object, saves the object to persistent storage, updates the * object registry, and replicates the object's current version information * to the relational db. In the case where it is not a deletion, the session * lock (TODO) is released, too. This happens as the result of a * writer.commit() call. */ public void doCommit(boolean cachedObjectRequired, Context context, DigitalObject obj, String logMessage, boolean remove) throws ServerException { // OBJECT REMOVAL... if (remove) { LOG.info("Committing removal of " + obj.getPid()); // DATASTREAM STORAGE: // remove any managed content datastreams associated with object // from persistent storage. Iterator<String> dsIDIter = obj.datastreamIdIterator(); while (dsIDIter.hasNext()) { String dsID = dsIDIter.next(); String controlGroupType = obj.datastreams(dsID).iterator().next().DSControlGrp; if (controlGroupType.equalsIgnoreCase("M")) { // iterate over all versions of this dsID for (Datastream dmc : obj.datastreams(dsID)) { String id = obj.getPid() + "+" + dmc.DatastreamID + "+" + dmc.DSVersionID; LOG.info("Deleting managed datastream: " + id); try { m_permanentStore.removeDatastream(id); } catch (LowlevelStorageException llse) { LOG.warn("Error attempting removal of managed " + "content datastream: ", llse); } } } } // STORAGE: // remove digital object from persistent storage try { m_permanentStore.removeObject(obj.getPid()); } catch (ObjectNotInLowlevelStorageException onilse) { LOG.warn("Object wasn't found in permanent low level " + "store, but that might be ok; continuing with purge"); } // INVALIDATE DOREADER CACHE: // now that the object xml is removed, make sure future requests // for the object will not use a stale copy if (m_readerCache != null) { m_readerCache.remove(obj.getPid()); } // REGISTRY: // Remove digital object from the registry try { unregisterObject(obj); } catch (ServerException se) { LOG .warn("Object couldn't be removed from registry, but that might be ok; continuing with purge"); } // FIELD SEARCH INDEX: // remove digital object from the default search index try { LOG.info("Deleting from FieldSearch index"); m_fieldSearch.delete(obj.getPid()); } catch (ServerException se) { LOG.warn("Object couldn't be removed from FieldSearch index (" + se.getMessage() + "), but that might be ok; continuing with purge"); } // RESOURCE INDEX: // remove digital object from the resourceIndex if (m_resourceIndex.getIndexLevel() != ResourceIndex.INDEX_LEVEL_OFF) { try { LOG.info("Deleting from ResourceIndex"); m_resourceIndex.deleteObject(new SimpleDOReader(null, null, null, null, null, obj)); LOG.debug("Finished deleting from ResourceIndex"); } catch (ServerException se) { LOG.warn("Object couldn't be removed from ResourceIndex (" + se.getMessage() + "), but that might be ok; continuing with purge"); } } // OBJECT INGEST (ADD) OR MODIFY... } else { if (obj.isNew()) { LOG.info("Committing addition of " + obj.getPid()); } else { LOG.info("Committing modification of " + obj.getPid()); } try { // DATASTREAM STORAGE: // copy and store any datastreams of type Managed Content Iterator<String> dsIDIter = obj.datastreamIdIterator(); while (dsIDIter.hasNext()) { String dsID = dsIDIter.next(); Datastream dStream = obj.datastreams(dsID).iterator() .next(); String controlGroupType = dStream.DSControlGrp; if (controlGroupType.equalsIgnoreCase("M")) // if it's managed, we might need to grab content { // iterate over all versions of this dsID for (Datastream dmc : obj.datastreams(dsID)) { if (URL_PROTOCOL.matcher(dmc.DSLocation).matches()) { // if it's a url, we need to grab content for this version MIMETypedStream mimeTypedStream; if (dmc.DSLocation.startsWith(DatastreamManagedContent.UPLOADED_SCHEME)) { mimeTypedStream = new MIMETypedStream(null, m_management.getTempStream(dmc.DSLocation), null); LOG .info("Getting managed datastream from internal uploaded " + "location: " + dmc.DSLocation); } else if (dmc.DSLocation.startsWith(DatastreamManagedContent.COPY_SCHEME)) { // make a copy of the pre-existing content mimeTypedStream = new MIMETypedStream(null, m_permanentStore .retrieveDatastream(dmc.DSLocation .substring(7)), null); } else if (dmc.DSLocation.startsWith(DatastreamManagedContent.TEMP_SCHEME)) { File file = new File(dmc.DSLocation .substring(7)); LOG .info("Getting base64 decoded datastream spooled from archive"); try { InputStream str = new FileInputStream(file); mimeTypedStream = new MIMETypedStream(dmc.DSMIME, str, null); } catch (FileNotFoundException fnfe) { LOG .warn("Unable to read temp file created for datastream from archive", fnfe); throw new StreamIOException("Error reading from temporary file created for binary content"); } } else { ContentManagerParams params = new ContentManagerParams(DOTranslationUtility .makeAbsoluteURLs(dmc.DSLocation .toString()),dmc.DSMIME,null,null); params.setContext(context); mimeTypedStream = m_contentManager.getExternalContent(params); LOG .info("Getting managed datastream from remote " + "location: " + dmc.DSLocation); } String id = obj.getPid() + "+" + dmc.DatastreamID + "+" + dmc.DSVersionID; if (obj.isNew()) { m_permanentStore .addDatastream(id, mimeTypedStream .getStream()); } else { // object already existed...so we may need to call // replace if "add" indicates that it was already there try { m_permanentStore .addDatastream(id, mimeTypedStream .getStream()); } catch (ObjectAlreadyInLowlevelStorageException oailse) { m_permanentStore .replaceDatastream(id, mimeTypedStream .getStream()); } } if (dmc.DSLocation.startsWith(DatastreamManagedContent.TEMP_SCHEME)) { // delete the temp file created to store the binary content from archive File file = new File(dmc.DSLocation .substring(7)); file.delete(); } // Reset dsLocation in object to new internal location. dmc.DSLocation = id; LOG .info("Replaced managed datastream location with " + "internal id: " + id); if(mimeTypedStream != null) { mimeTypedStream.close(); } } else { String id = obj.getPid() + "+" + dmc.DatastreamID + "+" + dmc.DSVersionID; LOG.warn("Inoperable DSLocation \"" + dmc.DSLocation + "\" given for " + id); } } } } // MANAGED DATASTREAM PURGE: // find out which, if any, managed datastreams were purged, // then remove them from low level datastream storage // this was moved because in the case of modifying a datastream // with versioning turned off, if a modification didn't involve new // content a special url of the form copy:... would be used to // indicate the content for the new datastream version, which would // point to the content of the most recent version. Which (if this code // had been executed earlier) would no longer exist in the low-level store. if (!obj.isNew()) { deletePurgedDatastreams(obj, context); } // MODIFIED DATE: // set digital object last modified date, in UTC obj.setLastModDate(Server.getCurrentDate(context)); ByteArrayOutputStream out = new ByteArrayOutputStream(); // FINAL XML SERIALIZATION: // serialize the object in its final form for persistent storage LOG.debug("Serializing digital object for persistent storage"); m_translator .serialize(obj, out, m_defaultStorageFormat, m_storageCharacterEncoding, DOTranslationUtility.SERIALIZE_STORAGE_INTERNAL); // FINAL VALIDATION: // As of version 2.0, final validation is only performed in DEBUG mode. // This is to help performance during the ingest process since validation // is a large amount of the overhead of ingest. Instead of a second run // of the validation module, we depend on the integrity of our code to // create valid XML files for persistent storage of digital objects. As // a sanity check, we check that we can deserialize the object we just serialized if (LOG.isDebugEnabled()) { ByteArrayInputStream inV = new ByteArrayInputStream(out.toByteArray()); LOG.debug("Final Validation (storage phase)"); m_validator.validate(inV, m_defaultStorageFormat, DOValidatorImpl.VALIDATE_ALL, "store"); } /* Verify that we can deserialize our object. */ m_translator .deserialize(new ByteArrayInputStream(out.toByteArray()), new BasicDigitalObject(), m_defaultStorageFormat, m_storageCharacterEncoding, DOTranslationUtility.SERIALIZE_STORAGE_INTERNAL); // RESOURCE INDEX: if (m_resourceIndex != null && m_resourceIndex.getIndexLevel() != ResourceIndex.INDEX_LEVEL_OFF) { LOG.info("Adding to ResourceIndex"); if (obj.isNew()) { m_resourceIndex.addObject(new SimpleDOReader(null, null, null, null, null, obj)); } else { m_resourceIndex.modifyObject(getReader(false, null, obj .getPid()), new SimpleDOReader(null, null, null, null, null, obj)); } LOG.debug("Finished adding to ResourceIndex."); } // STORAGE: // write XML serialization of object to persistent storage LOG.debug("Storing digital object"); if (obj.isNew()) { m_permanentStore.addObject(obj.getPid(), new ByteArrayInputStream(out .toByteArray())); } else { m_permanentStore.replaceObject(obj.getPid(), new ByteArrayInputStream(out .toByteArray())); } // INVALIDATE DOREADER CACHE: // now that the object xml is stored, make sure future DOReaders // will get the latest copy if (m_readerCache != null) { m_readerCache.remove(obj.getPid()); } // REGISTRY: /* * update systemVersion in doRegistry (add one), and update * deploymene maps if necesssary. */ LOG.debug("Updating registry"); Connection conn = null; Statement s = null; ResultSet results = null; try { conn = m_connectionPool.getConnection(); String query = "SELECT systemVersion " + "FROM doRegistry " + "WHERE doPID='" + obj.getPid() + "'"; s = conn.createStatement(); results = s.executeQuery(query); if (!results.next()) { throw new ObjectNotFoundException("Error creating replication job: The requested object doesn't exist in the registry."); } int systemVersion = results.getInt("systemVersion"); systemVersion++; s.executeUpdate("UPDATE doRegistry SET systemVersion=" + systemVersion + " " + "WHERE doPID='" + obj.getPid() + "'"); //TODO hasModel if (obj.hasContentModel(Models.SERVICE_DEPLOYMENT_3_0)) { updateDeploymentMap(obj, conn, false); } } catch (SQLException sqle) { throw new StorageDeviceException("Error creating replication job: " + sqle.getMessage()); } finally { try { if (results != null) { results.close(); } if (s != null) { s.close(); } if (conn != null) { m_connectionPool.free(conn); } } catch (SQLException sqle) { throw new StorageDeviceException("Unexpected error from SQL database: " + sqle.getMessage()); } finally { results = null; s = null; } } // REPLICATE: // add to replication jobs table and do replication to db LOG.info("Updating dissemination index"); String whichIndex = "FieldSearch"; try { LOG.info("Updating FieldSearch index"); m_fieldSearch.update(new SimpleDOReader(null, null, null, null, null, obj)); // FIXME: also remove from temp storage if this is successful // removeReplicationJob(obj.getPid()); } catch (ServerException se) { LOG.error("Error updating " + whichIndex + " index", se); throw se; } catch (Throwable th) { String msg = "Error updating " + whichIndex + " index"; LOG.error(msg, th); throw new GeneralException(msg, th); } } catch (Throwable th) { if (obj.isNew()) { // Clean up after a failed attempt to add try { doCommit(cachedObjectRequired, context, obj, logMessage, true); } catch (Exception e) { LOG.warn("Error while cleaning up after failed add", e); } } if (th instanceof ServerException) { throw (ServerException) th; } else { throw new GeneralException("Unable to add or modify object" + " (commit canceled)", th); } } } } private Set<Long> getDatastreamDates(Iterable<Datastream> ds) { Set<Long> dates = new HashSet<Long>(); for (Datastream d : ds) { dates.add(d.DSCreateDT.getTime()); } return dates; } private void deletePurgedDatastreams(DigitalObject obj, Context context) { try { // for each datastream that existed before the change: DOReader reader = getReader(false, context, obj.getPid()); Datastream[] datastreams = reader.GetDatastreams(null, null); for (Datastream element : datastreams) { // if it's a managed datastream... if (element.DSControlGrp.equals("M")) { String dsID = element.DatastreamID; /* * find out which versions were purged by comparing creation * dates. If a datastream is to be purged, then there won't * be one in the newest version of the object matching its * creation date and dsID. */ Set<Long> newVersionDates = getDatastreamDates(obj.datastreams(dsID)); Date[] dates = reader.getDatastreamVersions(dsID); for (Date dt : dates) { if (!newVersionDates.contains(dt.getTime())) { // ... and delete them from low level storage String token = obj.getPid() + "+" + dsID + "+" + reader.GetDatastream(dsID, dt).DSVersionID; try { m_permanentStore.removeDatastream(token); LOG.info("Removed purged datastream version " + "from low level storage (token = " + token + ")"); } catch (Exception e) { LOG.warn("Error removing purged datastream " + "version from low level storage " + "(token = " + token + ")", e); } } } } } } catch (ServerException e) { LOG .warn("Error reading " + obj.getPid() + "; if any" + " managed datastreams were purged, they were not removed " + " from low level storage.", e); } } /** * Gets the userId property from the context... if it's not populated, * throws an InvalidContextException. */ private String getUserId(Context context) throws InvalidContextException { String ret = context.getSubjectValue(Constants.SUBJECT.LOGIN_ID.uri); if (ret == null) { throw new InvalidContextException("The context identifies no userId, but a user must be identified for this operation."); } return ret; } /** * Checks the object registry for the given object. */ public boolean objectExists(String pid) throws StorageDeviceException { LOG.debug("Checking if " + pid + " already exists"); Connection conn = null; Statement s = null; ResultSet results = null; try { String query = "SELECT doPID " + "FROM doRegistry " + "WHERE doPID='" + pid + "'"; conn = m_connectionPool.getConnection(); s = conn.createStatement(); results = s.executeQuery(query); return results.next(); // 'true' if match found, else 'false' } catch (SQLException sqle) { throw new StorageDeviceException("Unexpected error from SQL database: " + sqle.getMessage()); } finally { try { if (results != null) { results.close(); } if (s != null) { s.close(); } if (conn != null) { m_connectionPool.free(conn); } } catch (SQLException sqle) { throw new StorageDeviceException("Unexpected error from SQL database: " + sqle.getMessage()); } finally { results = null; s = null; } } } public String getOwnerId(String pid) throws StorageDeviceException, ObjectNotFoundException { Connection conn = null; Statement s = null; ResultSet results = null; try { String query = "SELECT ownerId " + "FROM doRegistry " + "WHERE doPID='" + pid + "'"; conn = m_connectionPool.getConnection(); s = conn.createStatement(); results = s.executeQuery(query); if (results.next()) { return results.getString(1); } else { throw new ObjectNotFoundException("Object " + pid + " not found in object registry."); } } catch (SQLException sqle) { throw new StorageDeviceException("Unexpected error from SQL database: " + sqle.getMessage()); } finally { try { if (results != null) { results.close(); } if (s != null) { s.close(); } if (conn != null) { m_connectionPool.free(conn); } } catch (SQLException sqle) { throw new StorageDeviceException("Unexpected error from SQL database: " + sqle.getMessage()); } finally { results = null; s = null; } } } /** * Adds a new object. The caller *must* ensure the object does not already * exist in the registry before calling this method. */ private void registerObject(DigitalObject obj, String userId, String label, Date createDate, Date lastModDate) throws StorageDeviceException { String theLabel = label; String pid = obj.getPid(); if (theLabel == null) { theLabel = ""; } Connection conn = null; Statement st = null; try { String query = "INSERT INTO doRegistry (doPID, " + "ownerId, label) " + "VALUES ('" + pid + "', '" + userId + "', '" + SQLUtility.aposEscape(theLabel) + "')"; conn = m_connectionPool.getConnection(); st = conn.createStatement(); st.executeUpdate(query); } catch (SQLException sqle) { // clean up if the INSERT didn't succeeed try { unregisterObject(obj); } catch (Throwable th) { } // ...then notify the caller with the original exception throw new StorageDeviceException("Unexpected error from SQL database while registering object: " + sqle.getMessage()); } finally { try { if (st != null) { st.close(); } if (conn != null) { m_connectionPool.free(conn); } } catch (Exception sqle) { throw new StorageDeviceException("Unexpected error from SQL database while registering object: " + sqle.getMessage()); } finally { st = null; } } } /** * Removes an object from the object registry. */ private void unregisterObject(DigitalObject obj) throws StorageDeviceException { String pid = obj.getPid(); Connection conn = null; Statement st = null; try { conn = m_connectionPool.getConnection(); st = conn.createStatement(); st .executeUpdate("DELETE FROM doRegistry WHERE doPID='" + pid + "'"); //TODO hasModel if (obj.hasContentModel(Models.SERVICE_DEPLOYMENT_3_0)) { updateDeploymentMap(obj, conn, true); } } catch (SQLException sqle) { throw new StorageDeviceException("Unexpected error from SQL database while unregistering object: " + sqle.getMessage()); } finally { try { if (st != null) { st.close(); } if (conn != null) { m_connectionPool.free(conn); } } catch (Exception sqle) { throw new StorageDeviceException("Unexpected error from SQL database while unregistering object: " + sqle.getMessage()); } finally { st = null; } } } public String[] listObjectPIDs(Context context) throws StorageDeviceException { return getPIDs("WHERE systemVersion > 0"); } // translates simple wildcard string to sql-appropriate. // the first character is a " " if it needs an escape public static String toSql(String name, String in) { if (in.indexOf("\\") != -1) { // has one or more escapes, un-escape and translate StringBuffer out = new StringBuffer(); out.append("\'"); boolean needLike = false; boolean needEscape = false; boolean lastWasEscape = false; for (int i = 0; i < in.length(); i++) { char c = in.charAt(i); if (!lastWasEscape && c == '\\') { lastWasEscape = true; } else { char nextChar = '!'; boolean useNextChar = false; if (!lastWasEscape) { if (c == '?') { out.append('_'); needLike = true; } else if (c == '*') { out.append('%'); needLike = true; } else { nextChar = c; useNextChar = true; } } else { nextChar = c; useNextChar = true; } if (useNextChar) { if (nextChar == '\"') { out.append("\\\""); needEscape = true; } else if (nextChar == '\'') { out.append("\\\'"); needEscape = true; } else if (nextChar == '%') { out.append("\\%"); needEscape = true; } else if (nextChar == '_') { out.append("\\_"); needEscape = true; } else { out.append(nextChar); } } lastWasEscape = false; } } out.append("\'"); if (needLike) { out.insert(0, " LIKE "); } else { out.insert(0, " = "); } out.insert(0, name); if (needEscape) { out.insert(0, ' '); } return out.toString(); } else { // no escapes, just translate if needed StringBuffer out = new StringBuffer(); out.append("\'"); boolean needLike = false; boolean needEscape = false; for (int i = 0; i < in.length(); i++) { char c = in.charAt(i); if (c == '?') { out.append('_'); needLike = true; } else if (c == '*') { out.append('%'); needLike = true; } else if (c == '\"') { out.append("\\\""); needEscape = true; } else if (c == '\'') { out.append("\\\'"); needEscape = true; } else if (c == '%') { out.append("\\%"); needEscape = true; } else if (c == '_') { out.append("\\_"); needEscape = true; } else { out.append(c); } } out.append("\'"); if (needLike) { out.insert(0, " LIKE "); } else { out.insert(0, " = "); } out.insert(0, name); if (needEscape) { out.insert(0, ' '); } return out.toString(); } } /** whereClause is a WHERE clause, starting with "where" */ private String[] getPIDs(String whereClause) throws StorageDeviceException { ArrayList<String> pidList = new ArrayList<String>(); Connection conn = null; Statement s = null; ResultSet results = null; try { conn = m_connectionPool.getConnection(); s = conn.createStatement(); StringBuffer query = new StringBuffer(); query.append("SELECT doPID FROM doRegistry "); query.append(whereClause); LOG.debug("Executing db query: " + query.toString()); results = s.executeQuery(query.toString()); while (results.next()) { pidList.add(results.getString("doPID")); } String[] ret = new String[pidList.size()]; Iterator<String> pidIter = pidList.iterator(); int i = 0; while (pidIter.hasNext()) { ret[i++] = pidIter.next(); } return ret; } catch (SQLException sqle) { throw new StorageDeviceException("Unexpected error from SQL database: " + sqle.getMessage()); } finally { try { if (results != null) { results.close(); } if (s != null) { s.close(); } if (conn != null) { m_connectionPool.free(conn); } } catch (SQLException sqle) { throw new StorageDeviceException("Unexpected error from SQL database: " + sqle.getMessage()); } finally { results = null; s = null; } } } public FieldSearchResult findObjects(Context context, String[] resultFields, int maxResults, FieldSearchQuery query) throws ServerException { return m_fieldSearch.findObjects(resultFields, maxResults, query); } public FieldSearchResult resumeFindObjects(Context context, String sessionToken) throws ServerException { return m_fieldSearch.resumeFindObjects(sessionToken); } /** * <p> * Gets a list of the requested next available PIDs. the number of PIDs. * </p> * * @param numPIDs * The number of PIDs to generate. Defaults to 1 if the number is not * a positive integer. * @param namespace * The namespace to be used when generating the PIDs. If null, the * namespace defined by the <i>pidNamespace</i> parameter in the * fedora.fcfg configuration file is used. * @return An array of PIDs. * @throws ServerException * If an error occurs in generating the PIDs. */ public String[] getNextPID(int numPIDs, String namespace) throws ServerException { if (numPIDs < 1) { numPIDs = 1; } String[] pidList = new String[numPIDs]; if (namespace == null || namespace.equals("")) { namespace = m_pidNamespace; } try { for (int i = 0; i < numPIDs; i++) { pidList[i] = m_pidGenerator.generatePID(namespace).toString(); } return pidList; } catch (IOException ioe) { throw new GeneralException("DefaultDOManager.getNextPID: Error " + "generating PID, PIDGenerator returned unexpected error: (" + ioe.getClass().getName() + ") - " + ioe.getMessage()); } } public void reservePIDs(String[] pidList) throws ServerException { try { for (String element : pidList) { m_pidGenerator.neverGeneratePID(element); } } catch (IOException e) { throw new GeneralException("Error reserving PIDs", e); } } public String getRepositoryHash() throws ServerException { // This implementation returns a string containing the // total number of objects in the repository, followed by the // latest object's modification date (utc millis) // in the format: "10|194861293462" Connection conn = null; try { conn = m_connectionPool.getConnection(); StringBuffer hash = new StringBuffer(); hash.append(getNumObjectsWithVersion(conn, 0)); hash.append("|"); hash.append(getLatestModificationDate(conn, "")); return hash.toString(); } catch (SQLException e) { throw new GeneralException("SQL error encountered while computing " + "repository hash", e); } finally { if (conn != null) { m_connectionPool.free(conn); } } } /** * Get the number of objects in the registry whose system version is equal * to the given value. If n is less than one, return the total number of * objects in the registry. */ private int getNumObjectsWithVersion(Connection conn, int n) throws SQLException { Statement st = null; try { st = conn.createStatement(); StringBuffer query = new StringBuffer(); query.append("SELECT COUNT(*) FROM doRegistry"); if (n > 0) { query.append(" WHERE systemVersion = " + n); } ResultSet results = st.executeQuery(query.toString()); results.next(); return results.getInt(1); } finally { if (st != null) { st.close(); } } } private long getLatestModificationDate(Connection conn, String whereClause) throws SQLException { Statement st = null; try { st = conn.createStatement(); ResultSet results = st.executeQuery("SELECT MAX(mDate) " + "FROM doFields " + whereClause); if (results.next()) { return results.getLong(1); } else { return 0L; } } finally { if (st != null) { st.close(); } } } private class ModelDeploymentMap { private final Map<ServiceContext, Map<String, Long>> map = new ConcurrentHashMap<ServiceContext, Map<String, Long>>(); public String putDeployment(ServiceContext cxt, String sDep, long lastModDate) { if (!map.containsKey(cxt)) { map.put(cxt, new HashMap<String, Long>()); } map.get(cxt).put(sDep, lastModDate); return getDeployment(cxt); } /** Removes a deployment from a particular (cModel, sDef) context */ public String removeDeployment(ServiceContext cxt, String sDep) { Map<String, Long> deployments = map.get(cxt); if (deployments != null) { deployments.remove(sDep); } return getDeployment(cxt); } /** Return the OLDEST deployment for a given (cModel, sDef) context */ public String getDeployment(ServiceContext cxt) { if (map.containsKey(cxt)) { String sDep = null; int count = 0; long first = -1; for (Map.Entry<String, Long> dep : map.get(cxt).entrySet()) { if (dep.getValue() < first || first < 0) { first = dep.getValue(); sDep = dep.getKey(); count++; } } if (count > 1) { LOG .info("More than one service deployment specified for sDef " + cxt.sDef + " in model " + cxt.cModel + ". Using the one with the EARLIEST modification date."); } return sDep; } else { return null; } } /** * Return all the (cModel, sDef) contexts a serviceDeployment deploys * for */ public Set<ServiceContext> getContextFor(String sDep) { Set<ServiceContext> cxt = new HashSet<ServiceContext>(); for (Entry<ServiceContext, Map<String, Long>> dep : map.entrySet()) { if (dep.getValue().keySet().contains(sDep)) { cxt.add(dep.getKey()); } } return cxt; } } private static class ServiceContext { public final String cModel; public final String sDef; /* Internal string value for calculating hash code, equality */ private final String _val; private ServiceContext(String cModelPid, String sDefPid) { this.cModel = cModelPid; this.sDef = sDefPid; _val = "(" + cModelPid + "," + sDefPid + ")"; } public static ServiceContext getInstance(String cModel, String sDef) { return new ServiceContext(cModel, sDef); } @Override public String toString() { return _val; } @Override public boolean equals(Object o) { if (o == null) return false; if (!(o instanceof ServiceContext)) return false; return _val.equals(((ServiceContext) o)._val); } @Override public int hashCode() { return _val.hashCode(); } } }