/*
* Copyright (C) 2004 The Concord Consortium, Inc.,
* 10 Concord Crossing, Concord, MA 01742
*
* Web Site: http://www.concord.org
* Email: info@concord.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* END LICENSE */
/*
* Last modification information:
* $Revision: 1.43 $
* $Date: 2007-10-22 01:20:28 $
* $Author: scytacki $
*
* Licence Information
* Copyright 2004 The Concord Consortium
*/
package org.concord.otrunk.xml;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.logging.Logger;
import org.concord.framework.otrunk.OTID;
import org.concord.framework.otrunk.OTPackage;
import org.concord.framework.util.IResourceLoader;
import org.concord.framework.util.IResourceLoaderFactory;
import org.concord.otrunk.datamodel.BlobResource;
import org.concord.otrunk.datamodel.OTDataCollection;
import org.concord.otrunk.datamodel.OTDataList;
import org.concord.otrunk.datamodel.OTDataMap;
import org.concord.otrunk.datamodel.OTDataObject;
import org.concord.otrunk.datamodel.OTDataObjectType;
import org.concord.otrunk.datamodel.OTDataPropertyReference;
import org.concord.otrunk.datamodel.OTDatabase;
import org.concord.otrunk.datamodel.OTIDFactory;
import org.concord.otrunk.datamodel.OTPathID;
import org.concord.otrunk.datamodel.OTRelativeID;
import org.concord.otrunk.datamodel.OTTransientMapID;
import org.concord.otrunk.datamodel.OTUUID;
import org.concord.otrunk.net.IndentingPrintWriter;
import org.concord.otrunk.transfer.Transfer;
import org.concord.otrunk.view.OTConfig;
import org.concord.otrunk.xml.jdom.JDOMDocument;
/**
* XMLDatabase Class name and description
*
* Date created: Nov 19, 2004
*
* @author scott
* <p>
*
*/
public class XMLDatabase
implements OTDatabase
{
private static final Logger logger = Logger.getLogger(XMLDatabase.class.getName());
public static boolean TRACE_PACKAGES =
OTConfig.getBooleanProp(OTConfig.TRACE_PACKAGES_PROP, false);
public static boolean SILENT_DB = OTConfig.getBooleanProp(OTConfig.SILENT_DB, false);
OTID rootId = null;
ArrayList<String> importedOTObjectClasses = new ArrayList<String>();
HashMap<OTID, XMLDataObject> dataObjects = new HashMap<OTID, XMLDataObject>();
// a map of xml file ids to UUIDs
HashMap<String, OTID> localIdMap = new HashMap<String, OTID>();
// track whether any object in this database
// has changed
boolean dirty = false;
private OTID databaseId;
private ArrayList<Class<? extends OTPackage>> packageClasses =
new ArrayList<Class<? extends OTPackage>>();
private HashMap<String, Class<? extends OTPackage>> processedOTPackages =
new HashMap<String, Class<? extends OTPackage>>();
private boolean trackResourceInfo = false;
private JDOMDocument document;
private URL contextURL;
private URL sourceURL = null;
private ArrayList<XMLDatabaseChangeListener> listeners;
String label;
private HashMap<OTID, ArrayList<OTDataPropertyReference>> incomingReferences =
new HashMap<OTID, ArrayList<OTDataPropertyReference>>();
private HashMap<OTID, ArrayList<OTDataPropertyReference>> outgoingReferences =
new HashMap<OTID, ArrayList<OTDataPropertyReference>>();
private long urlOpenTime;
private long downloadTime = -1;
private long parseTime;
private boolean sourceVerified = false;
private long urlLastModifiedTime = -1;
private int urlContentLength = -1;
private IResourceLoader resourceLoader;
private static IResourceLoaderFactory resourceLoaderFactory =
new org.concord.otrunk.net.OTrunkResourceLoaderFactory();
protected static String getLabel(URL contextURL)
{
if (contextURL == null) {
return "unknown_name";
}
return contextURL.toExternalForm();
}
public XMLDatabase()
{
// create an empty database with no root
}
public XMLDatabase(File xmlFile) throws Exception
{
this(new FileInputStream(xmlFile), xmlFile.toURL(), null);
}
public XMLDatabase(URL xmlURL) throws Exception
{
this(xmlURL, SILENT_DB ? null : System.out);
}
public XMLDatabase(URL xmlURL, PrintStream statusStream) throws Exception
{
this(xmlURL, false, statusStream);
}
/**
* This will print information about the url if there is an exception loading it. This should
* be refactored because in some cases this method might be used to see if the URL exists.
* So it will throw a 404 and that shouldn't be printed.
*
* @param xmlURL
* @param required If implemented correctly required might prompt the user to retry the url
* and if that fails it will throw an Error with the intention of stopping the application.
* @param statusStream
* @throws Exception
*/
public XMLDatabase(URL xmlURL, boolean promptRetryQuit, PrintStream statusStream) throws Exception
{
initialize(xmlURL, xmlURL.toExternalForm(), statusStream);
long openingStart = System.currentTimeMillis();
InputStream urlInStream = null;
resourceLoader = resourceLoaderFactory.getResourceLoader(xmlURL, promptRetryQuit);
try {
urlInStream = resourceLoader.getInputStream();
} catch (Exception e) {
printErrorDetails();
throw e;
}
urlLastModifiedTime = resourceLoader.getLastModified();
urlContentLength = resourceLoader.getContentLength();
urlOpenTime = System.currentTimeMillis() - openingStart;
// parse the xml file...
long startMillis = -1;
JDOMDocument xmlDocument = null;
InputStream inputStream = urlInStream;
if(OTConfig.getBooleanProp(OTConfig.TRACE_DB_LOAD_TIME, false)){
long transferStart = System.currentTimeMillis();
Transfer transfer = new Transfer();
ByteArrayOutputStream byteCache = new ByteArrayOutputStream();
transfer.transfer(urlInStream, byteCache);
inputStream = new ByteArrayInputStream(byteCache.toByteArray());
downloadTime = System.currentTimeMillis() - transferStart;
}
startMillis = System.currentTimeMillis();
try {
xmlDocument = new JDOMDocument(inputStream);
} catch (Exception e){
printErrorDetails();
throw e;
}
parseTime = System.currentTimeMillis() - startMillis;
initializeDoc(xmlDocument);
}
public void printErrorDetails()
{
if (! SILENT_DB) {
IndentingPrintWriter writer = new IndentingPrintWriter(System.err);
writer.printFirstln("Error Loading XMLDatabase: ");
resourceLoader.writeResourceErrorDetails(writer, true);
writer.flush();
}
}
public XMLDatabase(InputStream xmlStream, URL contextURL, PrintStream statusStream)
throws Exception
{
// The "" + approach allows us to handle nulls.
this(xmlStream, contextURL, getLabel(contextURL), statusStream);
}
public XMLDatabase(InputStream xmlStream, URL contextURL, String label,
PrintStream statusStream) throws Exception
{
initialize(contextURL, label, statusStream);
// printStatus("Opening otml: " + label);
// parse the xml file...
long startMillis = -1;
JDOMDocument xmlDocument = null;
try {
InputStream inputStream = xmlStream;
if(OTConfig.getBooleanProp(OTConfig.TRACE_DB_LOAD_TIME, false)){
long transferStart = System.currentTimeMillis();
Transfer transfer = new Transfer();
ByteArrayOutputStream byteCache = new ByteArrayOutputStream();
transfer.transfer(xmlStream, byteCache);
inputStream = new ByteArrayInputStream(byteCache.toByteArray());
downloadTime = System.currentTimeMillis() - transferStart;
}
startMillis = System.currentTimeMillis();
xmlDocument = new JDOMDocument(inputStream);
} catch (Exception e) {
if (contextURL != null) {
URLConnection connection = contextURL.openConnection();
if (connection instanceof HttpURLConnection) {
logger.severe("Error created xml document");
logger.severe("Response code for " + contextURL + ":");
logger.severe(" "
+ ((HttpURLConnection) connection).getResponseCode());
}
logger.severe("Length of xmlstream from " + contextURL + ":");
logger.severe(" " + connection.getInputStream().available());
}
throw e;
}
parseTime = System.currentTimeMillis() - startMillis;
initializeDoc(xmlDocument);
}
public XMLDatabase(Reader xmlReader, URL contextURL, PrintStream statusStream)
throws Exception
{
this(xmlReader, contextURL, getLabel(contextURL), statusStream);
}
public XMLDatabase(Reader xmlReader, URL contextURL, String label,
PrintStream statusStream) throws Exception
{
initialize(contextURL, label, statusStream);
// printStatus("Opening otml: " + label);
long startMillis = System.currentTimeMillis();
JDOMDocument xmlDocument = new JDOMDocument(xmlReader);
parseTime = System.currentTimeMillis() - startMillis;
initializeDoc(xmlDocument);
}
@Override
public boolean equals(Object object)
{
if (object == this) {
return true;
}
if (!(object instanceof XMLDatabase)) {
return false;
}
OTID id = getDatabaseId();
if (id == null) {
return false;
}
return id.equals(((XMLDatabase) object).getDatabaseId());
}
/**
* @deprecated use initialize without the statusStream
* @param contextURL
* @param label
* @param statusStream
*/
@Deprecated
protected void initialize(URL contextURL, String label, PrintStream statusStream)
{
initialize(contextURL, label);
}
protected void initialize(URL contextURL, String label)
{
this.label = label;
// create the database Id
// this might get overriden when the objects are loaded in.
databaseId = OTUUID.createOTUUID();
// The order here matters because initialize will look at the codebase attribute of the otrunk
// element, and set the contextURL to be that. That codebase attribute should override
// the passed in contextURL
this.contextURL = contextURL;
setSourceURL(contextURL);
}
protected void initializeDoc(JDOMDocument document)
{
this.document = document;
OTXMLElement rootElement = document.getRootElement();
String dbCodeBase = rootElement.getAttributeValue("codebase");
// the system property overrides what is in the xml document
String systemCodeBase = OTConfig.getStringProp(OTConfig.CODEBASE_PROP);
if (systemCodeBase != null) {
dbCodeBase = systemCodeBase;
}
if (dbCodeBase != null && dbCodeBase.length() > 0) {
// this document has a specific base address
// make sure the address ends with a slash
if (!dbCodeBase.endsWith("/")) {
dbCodeBase += "/";
}
// add a pseudo file to the end so the URL class treats
// this as a correct contextURL: it strips off the last part
// of the context url.
dbCodeBase += "pseudo.txt";
try {
contextURL = new URL(dbCodeBase);
} catch (MalformedURLException e) {
// the base url was not formed right
e.printStackTrace();
}
}
String dbId = rootElement.getAttributeValue("id");
if (dbId != null && dbId.length() > 0) {
databaseId = OTIDFactory.createOTID(dbId);
}
// Everything is loaded, so we know the sourceURL was valid
setSourceVerified(true);
}
public void loadObjects()
throws Exception
{
long startMillis = System.currentTimeMillis();
if (document == null) {
throw new RuntimeException("Database was not initialized!");
}
OTXMLElement rootElement = document.getRootElement();
// don't keep the document around. it tends to use a lot of memory and isn't used once the objects are loaded.
document = null;
TypeService typeService = new TypeService(contextURL);
ObjectTypeHandler objectTypeHandler = new ObjectTypeHandler(typeService, this);
typeService.registerUserType("object", objectTypeHandler);
OTXMLElement importsElement = rootElement.getChild("imports");
if (importsElement == null) {
throw new RuntimeException("<imports> element is missing");
}
List<?> imports = importsElement.getChildren();
for (Iterator<?> iterator = imports.iterator(); iterator.hasNext();) {
OTXMLElement currentImport = (OTXMLElement) iterator.next();
String className = currentImport.getAttributeValue("class");
importedOTObjectClasses.add(className);
// TODO look for package classes based on thise imports and
// save them. The OTrunkImpl will then ask for these packages from the
// the database when it loads it and then initialize the packages
Class<? extends OTPackage> packageClass = findPackageClass(className);
if (packageClass != null && !packageClasses.contains(packageClass)) {
packageClasses.add(packageClass);
}
}
ReflectionTypeDefinitions.registerTypes(importedOTObjectClasses, typeService,
this);
// Add local_ids listed in the otml's idMap to the localIdMap
// This way the objects with a defined uuid can be referred to
// by a human-readable local_id within the otml file
OTXMLElement idMapElement = rootElement.getChild("idMap");
if (idMapElement != null) {
List<?> idMappings = idMapElement.getChildren();
for (Iterator<?> it = idMappings.iterator(); it.hasNext();) {
OTXMLElement mapping = (OTXMLElement) it.next();
String idStr = mapping.getAttributeValue("id");
String localIdStr = mapping.getAttributeValue("local_id");
OTID otid = OTIDFactory.createOTID(idStr);
Object oldId = localIdMap.put(localIdStr, otid);
if (oldId != null) {
logger.warning("repeated local id: " + localIdStr);
}
}
}
// Pass 1:
// Load all the xml data objects in the file
// This also makes a list of all these objects so we
// can handle them linearly in the next pass.
OTXMLElement objects = rootElement.getChild("objects");
List<?> xmlObjects = objects.getChildren();
if (xmlObjects.size() != 1) {
throw new Exception("Can only load files that contain a single root object");
}
OTXMLElement rootObjectNode = (OTXMLElement) xmlObjects.get(0);
// Recursively load all the data objects
// If the database does not have an id then use a path of anon_root
// Due to an error before, anon_root was being used even when the database had an
// id. However all of our legacy data came from files that used local_ids everywhere.
// so in that case the correct database id is used.
String relativePath = "anon_root";
if (databaseId != null) {
relativePath = databaseId.toExternalForm() + "/";
}
XMLDataObject rootDataObject =
(XMLDataObject) typeService.handleLiteralElement(rootObjectNode, relativePath, null, null);
// Need to handle local_id this will be stored as XMLDataObjectRef with in the
// tree. this is what the objectReferences vector is for
// each references stores the source object and the key within that object
// where the object should be stored.
secondPass();
setRoot(rootDataObject.getGlobalId());
long endMillis = System.currentTimeMillis();
String parsedLabel = "downloaded and parsed xml";
String downloadString = "";
if(downloadTime >= 0){
downloadString = " downloaded in " + downloadTime + "ms";
parsedLabel = "parsed xml";
}
String sizeString = " (unknown size)";
if (urlContentLength > -1) {
sizeString = " (" + urlContentLength + " bytes)";
}
logger.info("Loaded " + dataObjects.size() + " objects from: " + label
+ sizeString + " opened url in " + urlOpenTime + "ms"
+ downloadString
+ " " + parsedLabel + " in " + parseTime + "ms"
+ " loaded ot db in " + (endMillis - startMillis) + "ms" );
// release the resources associated with the XML DOM elements
for (XMLDataObject obj : dataObjects.values()) {
obj.nullifyElement();
}
}
public static abstract class PackageNotFound implements OTPackage
{
};
/**
* This will take a imported class name and figure out the name of the OT
* package class. If for example the className is
* org.concord.datagraph.state.OTDataCollector It looks for a class called:
* org.concord.datagraph.state.OTDatagraphPackage
*
* This is figured out by Taking off the classname
* org.concord.datagraph.state striping off the .state (if there is one)
* org.concord.datagraph taking the last element of the package name
* datagraph capitalizing the first leter and adding OT to the front and
* package to back OTDatagraphPackage using the original package of the
* imported class org.concord.datagraph.state.OTDatagraphPackage
*
* @param className
* @return
*/
@SuppressWarnings("unchecked")
private Class<? extends OTPackage> findPackageClass(String className)
{
int lastDot = className.lastIndexOf('.');
String packageName = className.substring(0, lastDot);
Class<? extends OTPackage> otPackageClass;
otPackageClass = processedOTPackages.get(packageName);
if (otPackageClass == PackageNotFound.class) {
// we looked for this package before but couldn't find it
return null;
} else if (otPackageClass != null) {
return otPackageClass;
}
String otPackageStr = packageName;
if (packageName.endsWith(".state")) {
otPackageStr =
packageName.substring(0, packageName.length() - ".state".length());
}
String capitalizedOTPackageStr = null;
// Special case org.concord packages
if (otPackageStr.startsWith("org.concord.")) {
otPackageStr = otPackageStr.substring("org.concord.".length());
String newOTPackageStr = "";
int curIndex = 0;
while (curIndex < otPackageStr.length()) {
int nextIndex = otPackageStr.indexOf('.', curIndex);
if (nextIndex == -1) {
nextIndex = otPackageStr.length();
}
newOTPackageStr +=
otPackageStr.substring(curIndex, curIndex + 1).toUpperCase()
+ otPackageStr.substring(curIndex + 1, nextIndex);
curIndex = nextIndex + 1;
}
capitalizedOTPackageStr = newOTPackageStr;
} else {
lastDot = otPackageStr.lastIndexOf('.');
otPackageStr = otPackageStr.substring(lastDot + 1);
capitalizedOTPackageStr =
otPackageStr.substring(0, 1).toUpperCase() + otPackageStr.substring(1);
}
String otPackageClassName = "OT" + capitalizedOTPackageStr + "Package";
String fullyQualifiedOTPackageClassName = packageName + "." + otPackageClassName;
try {
otPackageClass = (Class<? extends OTPackage>)
getClass().getClassLoader().loadClass(fullyQualifiedOTPackageClassName);
if (TRACE_PACKAGES) {
logger.info("loaded package: " + otPackageClass);
}
processedOTPackages.put(packageName, otPackageClass);
return otPackageClass;
} catch (ClassNotFoundException e) {
if (TRACE_PACKAGES) {
logger.info("no OTPackage for: " + packageName);
logger.info(" the classname should be: "
+ fullyQualifiedOTPackageClassName);
}
// add to a list of notfound otpackages so we don't look for it
// again
processedOTPackages.put(packageName, PackageNotFound.class);
}
return null;
}
/*
* (non-Javadoc)
*
* @see
* org.concord.otrunk.datamodel.OTDatabase#setRoot(org.doomdark.uuid.UUID)
*/
public void setRoot(OTID rootId)
throws Exception
{
this.rootId = rootId;
}
/*
* (non-Javadoc)
*
* @see org.concord.otrunk.datamodel.OTDatabase#getRoot()
*/
public OTDataObject getRoot()
throws Exception
{
if (rootId == null) {
return null;
}
return dataObjects.get(rootId);
}
public OTID getDatabaseId()
{
return databaseId;
}
public HashMap<OTID, XMLDataObject> getDataObjects()
{
return dataObjects;
}
public boolean isDirty()
{
return dirty;
}
public void setDirty(boolean dirty)
{
boolean oldValue = this.dirty;
this.dirty = dirty;
if (oldValue != this.dirty) {
notifyListeners();
}
}
private void notifyListeners()
{
if (listeners == null)
return;
XMLDatabaseChangeEvent changeEvent = new XMLDatabaseChangeEvent(this);
String status =
dirty ? XMLDatabaseChangeEvent.STATE_DIRTY
: XMLDatabaseChangeEvent.STATE_CLEAN;
changeEvent.setValue(status);
for(XMLDatabaseChangeListener listener: listeners){
listener.stateChanged(changeEvent);
}
}
public void addXMLDatabaseChangeListener(XMLDatabaseChangeListener listener)
{
if (listener == null) {
throw new IllegalArgumentException("listener cannot be null");
}
if (listeners == null) {
listeners = new ArrayList<XMLDatabaseChangeListener>();
}
listeners.add(listener);
}
protected XMLDataObject createDataObject(OTXMLElement element, String idStr)
throws Exception
{
OTID id = null;
if (idStr != null) {
id = OTIDFactory.createOTID(idStr);
}
return createDataObject(element, id);
}
protected XMLDataObject createDataObject(OTXMLElement element, OTID id)
throws Exception
{
logger.finest("Creating data object for: " + (id == null ? "null" : id.toExternalForm()) );
if (id == null) {
// String path = TypeService.elementPath(element);
// id = new OTXMLPathID(path);
id = OTUUID.createOTUUID();
}
XMLDataObject dataObject = new XMLDataObject(element, id, this);
XMLDataObject oldValue = dataObjects.put(dataObject.getGlobalId(), dataObject);
if (oldValue != null) {
dataObjects.put(dataObject.getGlobalId(), oldValue);
throw new Exception("repeated unique id: "
+ dataObject.getGlobalId().toExternalForm());
}
if (element != null) {
String localIdStr = element.getAttributeValue("local_id");
if (localIdStr != null && localIdStr.length() > 0) {
dataObject.setLocalId(localIdStr);
// this is probably a temporary hack
// we want to save local id so it can be shown
// to the author. It is useful for debugging
dataObject.setResource("localId", localIdStr);
Object oldId = localIdMap.put(localIdStr, dataObject.getGlobalId());
if (oldId != null) {
logger.warning("repeated local id: " + localIdStr);
}
}
}
return dataObject;
}
/*
* (non-Javadoc)
*
* @see org.concord.otrunk.datamodel.OTDatabase#createDataObject()
*/
public OTDataObject createDataObject(OTDataObjectType type)
throws Exception
{
XMLDataObject xmlDataObject = createDataObject((OTXMLElement) null, (OTID) null);
xmlDataObject.setType(type);
return xmlDataObject;
}
/*
* (non-Javadoc)
*
* @see
* org.concord.otrunk.datamodel.OTDatabase#createDataObject(org.doomdark
* .uuid.UUID)
*/
public OTDataObject createDataObject(OTDataObjectType type, OTID id)
throws Exception
{
XMLDataObject xmlDataObject = createDataObject((OTXMLElement) null, id);
xmlDataObject.setType(type);
return xmlDataObject;
}
/*
* (non-Javadoc)
*
* @see
* org.concord.otrunk.datamodel.OTDatabase#createCollection(org.concord.
* otrunk.datamodel.OTDataObject, java.lang.Class)
*/
public OTDataCollection createCollection(OTDataObject parent, Class<?> collectionClass)
throws Exception
{
if (collectionClass.equals(OTDataList.class)) {
return new XMLDataList((XMLDataObject) parent);
} else if (collectionClass.equals(OTDataMap.class)) {
return new XMLDataMap((XMLDataObject) parent);
}
return null;
}
/*
* (non-Javadoc)
*
* @see
* org.concord.otrunk.datamodel.OTDatabase#getOTDataObject(org.concord.otrunk
* .datamodel.OTDataObject, org.doomdark.uuid.UUID)
*/
public OTDataObject getOTDataObject(OTDataObject dataParent, OTID childID)
throws Exception
{
// we are going to ignore the dataParent for now
return dataObjects.get(childID);
}
/*
* (non-Javadoc)
*
* @see
* org.concord.otrunk.datamodel.OTDatabase#contains(org.concord.framework
* .otrunk.OTID)
*/
public boolean contains(OTID id)
{
return id.equals(databaseId) || dataObjects.containsKey(id);
}
/*
* (non-Javadoc)
*
* @see org.concord.otrunk.datamodel.OTDatabase#close()
*/
public void close()
{
// TODO Auto-generated method stub
// resave the xml file maybe???
}
public void secondPass()
throws Exception
{
Collection<XMLDataObject> objects = dataObjects.values();
for (Iterator<XMLDataObject> iter = objects.iterator(); iter.hasNext();) {
XMLDataObject xmlDObj = iter.next();
if (xmlDObj instanceof XMLDataObjectRef) {
throw new Exception("Found a reference in object list");
}
Collection<Entry<String, Object>> entries = xmlDObj.getResourceEntries();
ArrayList<String> removedKeys = new ArrayList<String>();
for(Entry<String, Object> resourceEntry: entries){
Object resourceValue = resourceEntry.getValue();
Object newResourceValue = null;
String resourceKey = resourceEntry.getKey();
logger.finest("Processing key: " + resourceKey + ", with value: " + resourceValue);
if (resourceValue instanceof XMLDataObject) {
XMLDataObject resourceValueObj = (XMLDataObject) resourceValue;
newResourceValue = getOTID(resourceValueObj);
if (newResourceValue == null) {
removedKeys.add(resourceKey);
} else {
xmlDObj.setResource(resourceKey, newResourceValue, false);
recordSecondPassReference(resourceValueObj);
}
} else if (resourceValue instanceof XMLDataList) {
XMLDataList list = (XMLDataList) resourceValue;
for (int j = 0; j < list.size(); j++) {
Object oldElement = list.get(j);
if (oldElement instanceof XMLDataObject) {
XMLDataObject oldElementObj = (XMLDataObject) oldElement;
OTID newElement = getOTID((XMLDataObject) oldElement);
list.set(j, newElement);
recordSecondPassReference(oldElementObj);
}
if (oldElement instanceof XMLParsableString) {
newResourceValue =
((XMLParsableString) oldElement).parse(localIdMap);
list.set(j, newResourceValue);
}
}
// the resource list value doesn't need to be updated
} else if (resourceValue instanceof XMLDataMap) {
XMLDataMap map = (XMLDataMap) resourceValue;
String[] keys = map.getKeys();
for (int j = 0; j < keys.length; j++) {
Object oldElement = map.get(keys[j]);
// Check if the key is a local id reference
// if it is then replace it with the string
// representation of this key
if (keys[j].startsWith("${")) {
OTID globalId = getGlobalId(keys[j]);
if (globalId != null) {
map.remove(keys[j]);
keys[j] = globalId.toExternalForm();
map.put(keys[j], oldElement);
}
}
if (oldElement instanceof XMLDataObject) {
XMLDataObject oldElementObj = (XMLDataObject) oldElement;
OTID newElement = getOTID((XMLDataObject) oldElement);
map.put(keys[j], newElement);
// update the references map
recordSecondPassReference(oldElementObj);
}
if (oldElement instanceof XMLParsableString) {
newResourceValue =
((XMLParsableString) oldElement).parse(localIdMap);
map.put(keys[j], newResourceValue);
}
}
} else if (resourceValue instanceof XMLParsableString) {
// replace the local ids from the string
newResourceValue =
((XMLParsableString) resourceValue).parse(localIdMap);
xmlDObj.setResource(resourceKey, newResourceValue);
} else {
logger.finest("Not valid object type: " + resourceValue);
}
}
// remove the keys that have null values
// this can't be done in the previous loop because that screws up
// the
// the Iterator
for (int keyIndex = 0; keyIndex < removedKeys.size(); keyIndex++) {
xmlDObj.setResource(removedKeys.get(keyIndex), null);
}
}
}
private void recordSecondPassReference(XMLDataObject dataObject)
{
if(dataObject instanceof XMLDataObjectRef){
OTID otid = getOTID(dataObject);
XMLDataObjectRef ref = (XMLDataObjectRef) dataObject;
if(ref.parent == null){
// scytacki: I'm not sure this will ever happen
logger.finest("Parent was null (object): " + otid);
} else {
recordReference(ref.parent.getGlobalId(), otid, ref.property);
}
}
}
private OTID getGlobalId(String idStr)
{
if (idStr.startsWith("${")) {
if(!idStr.endsWith("}")){
logger.warning("local id reference must end with }: " + idStr.substring(2,idStr.length()));
return null;
}
String localId = idStr.substring(2, idStr.length() - 1);
OTID globalId = localIdMap.get(localId);
if (globalId == null) {
logger.warning("Can't find local id: " + localId);
}
return globalId;
} else {
return OTIDFactory.createOTID(idStr);
}
}
private OTID getOTID(XMLDataObject xmlDObj)
{
if (xmlDObj instanceof XMLDataObjectRef) {
String refId = ((XMLDataObjectRef) xmlDObj).getRefId();
return getGlobalId(refId);
}
return xmlDObj.getGlobalId();
}
public HashMap<String, OTID> getLocalIDMap()
{
return localIdMap;
}
/**
* @param localIdStr
* @return
*/
public OTID getOTIDFromLocalID(String localIdStr)
{
// if the db has a id use that plus
// the local id to create a relative ID and change
// the contains method to include that database id
OTID dbId = getDatabaseId();
if (dbId != null) {
return new OTRelativeID(dbId, new OTPathID("/" + localIdStr));
}
// FIXME
// if the databse doesn't have a id then we use some
// standard anon relative id (I don't know if that will
// work) otherwise we could hash something into an id
return new OTRelativeID(null, new OTPathID("/" + localIdStr));
}
/*
* (non-Javadoc)
*
* @see
* org.concord.otrunk.datamodel.OTDatabase#createBlobResource(java.net.URL)
*/
public BlobResource createBlobResource(URL url)
{
return new BlobResource(url);
}
/*
* (non-Javadoc)
*
* @see org.concord.otrunk.datamodel.OTDatabase#getPackageClasses()
*/
@SuppressWarnings("unchecked")
public ArrayList<Class<? extends OTPackage>> getPackageClasses()
{
return (ArrayList<Class<? extends OTPackage>>) packageClasses.clone();
}
public boolean isTrackResourceInfo()
{
return trackResourceInfo;
}
public void setTrackResourceInfo(boolean trackResourceInfo)
{
this.trackResourceInfo = trackResourceInfo;
}
public ArrayList<String> getImportedOTObjectClasses()
{
return importedOTObjectClasses;
}
public URL getContextURL()
{
return contextURL;
}
public void recordReference(OTDataObject parent, OTDataObject child, String property)
{
if (parent == null || child == null) {
// can't reference "null"
return;
}
if (child instanceof XMLDataObjectRef) {
// this will be handled in the second pass
return;
}
OTID parentID = parent.getGlobalId();
OTID childID = child.getGlobalId();
recordReference(parentID, childID, property);
}
public void recordReference(OTID parentID, OTID childID, String property) {
if (parentID == null || childID == null) {
// can't reference null
return;
}
parentID = parentID.getMappedId();
childID = childID.getMappedId();
logger.finer("Recording reference: " + parentID + " (" + property + ") --> " + childID);
OTDataPropertyReference ref = new OTDataPropertyReference(parentID, childID, property);
ArrayList<OTDataPropertyReference> parents = incomingReferences.get(childID);
ArrayList<OTDataPropertyReference> children = outgoingReferences.get(parentID);
if (parents == null) {
parents = new ArrayList<OTDataPropertyReference>();
}
if (children == null) {
children = new ArrayList<OTDataPropertyReference>();
}
if (! parents.contains(ref)) {
parents.add(ref);
incomingReferences.put(childID, parents);
}
if (! children.contains(ref)) {
children.add(ref);
outgoingReferences.put(parentID, children);
}
}
public void removeReference(OTDataObject parent, OTDataObject child) {
if (parent == null || child == null) {
// can't reference null
return;
}
OTID parentID = parent.getGlobalId();
OTID childID = child.getGlobalId();
removeReference(parentID, childID);
}
public void removeReference(OTID parentID, OTID childID) {
if (parentID == null || childID == null) {
// can't reference null
return;
}
parentID = parentID.getMappedId();
childID = childID.getMappedId();
logger.finer("Removing reference: " + parentID + " --> " + childID);
ArrayList<OTDataPropertyReference> parents = incomingReferences.get(childID);
ArrayList<OTDataPropertyReference> children = outgoingReferences.get(parentID);
if (parents != null) {
OTDataPropertyReference refToRemove = null;
for (OTDataPropertyReference ref : parents) {
if (ref.getSource().equals(parentID) && ref.getDest().equals(childID)) {
refToRemove = ref;
break;
}
}
if (refToRemove != null) {
parents.remove(refToRemove);
incomingReferences.put(childID, parents);
}
}
if (children != null) {
OTDataPropertyReference refToRemove = null;
for (OTDataPropertyReference ref : children) {
if (ref.getSource().equals(parentID) && ref.getDest().equals(childID)) {
refToRemove = ref;
break;
}
}
if (refToRemove != null) {
children.remove(refToRemove);
outgoingReferences.put(parentID, children);
}
}
}
/**
* This returns the URL which points to the location that this db came from or should be saved to.
* It is similar to the contextURL, except that this won't be changed if the source resets the codebase.
*
* @return URL The URL from which this db was loaded, or to which this db has/will be persisted
*/
public URL getSourceURL() {
return this.sourceURL;
}
/**
* Set a new location for this db to be persisted. Setting this will cause the source to
* be unverified since it does no checking to make sure the URL points to a valid, accessible location.
*
* @param source URL The location to which this db should be persisted.
*/
public void setSourceURL(URL source) {
this.sourceURL = source;
this.sourceVerified = false;
}
/**
* Denotes if the sourceURL is known to be a valid, accessible location.
*
* @return boolean true if the sourceURL is valid and accessible, false if it's not valid/accessible or it's unknown
*/
public boolean isSourceVerified() {
return this.sourceVerified;
}
/**
* Sets whether the sourceURL is valid and accessible. This should only be set once the db has been successfully persisted
* to the sourceURL!
*
* @param verified boolean true if the sourceURL is valid and accessible, false if it's not valid/accessible or it's unknown
*/
public void setSourceVerified(boolean verified) {
this.sourceVerified = verified;
}
/**
* Sets the time that the document located at the sourceURL was last modified, or 0 if unknown
* @param urlLastModifiedTime the urlLastModifiedTime to set
*/
public void setUrlLastModifiedTime(long urlLastModifiedTime)
{
this.urlLastModifiedTime = urlLastModifiedTime;
}
/**
* The time at which the sourceURL document was last modified, in ms since Jan 1, 1970, or 0 if unknown. This can be used to determine if an update is needed.
* @return the urlLastModifiedTime
*/
public long getUrlLastModifiedTime()
{
return urlLastModifiedTime;
}
public static void setResourceLoaderFactory(IResourceLoaderFactory factory) {
resourceLoaderFactory = factory;
}
/**
* This verifies the particular object is valid to add to this database.
* If not it throws a runtime exception.
* Invalid objects are transient otobjects, or objects that are not primitives, otid, lists,
* maps, or blobs.
* @param obj
*/
final static void checkObject(Object obj)
{
if(obj instanceof OTTransientMapID){
throw new RuntimeException("Can't add transient id to XMLDatabase. id: " +
((OTTransientMapID)obj).toInternalForm());
}
}
public URI getURI()
{
URL srcURL = getSourceURL();
try {
if(srcURL != null){
return srcURL.toURI();
} else {
return new URI("xml-db:/" + getDatabaseId());
}
} catch (URISyntaxException e) {
e.printStackTrace();
}
return null;
}
public ArrayList<OTDataPropertyReference> getIncomingReferences(OTID otid)
{
return incomingReferences.get(otid);
}
public ArrayList<OTDataPropertyReference> getOutgoingReferences(OTID otid)
{
return outgoingReferences.get(otid);
}
}