/*
* Copyright 2003-2010 Tufts University Licensed under the
* Educational Community License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.osedu.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package edu.tufts.vue.dsm.impl;
import java.io.*;
import java.util.*;
import java.net.*;
import tufts.Util;
import org.osid.repository.Repository;
import edu.tufts.vue.dsm.DataSource;
import edu.tufts.vue.dsm.DataSourceListener;
//classes to support marshalling and unmarshalling
import org.exolab.castor.xml.Marshaller;
import org.exolab.castor.xml.Unmarshaller;
import org.exolab.castor.xml.MarshalException;
import org.exolab.castor.xml.ValidationException;
import org.exolab.castor.mapping.Mapping;
import org.exolab.castor.mapping.MappingException;
import org.xml.sax.InputSource;
/**
*
* This class loads and saves Data Source content from an XML file, provides
* for multi-threaded hang-proof initialization (repository configuration),
* and event delivery to track the progress of loading.
*
* @version $Revision: 1.53 $ / $Date: 2010-02-03 19:25:34 $ / $Author: mike $
*/
public class VueDataSourceManager
implements edu.tufts.vue.dsm.DataSourceManager
{
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(VueDataSourceManager.class);
/** If true, all data sources will block until their repositories are found and
* configured, which can result in a hang if there is a problem with any repository.
* This is the original implementation, and remains as a fallback impl in case of
* threading issues at startup, but VUE's relevant impl's should now fully handle
* the multi-threaded dynamic event delivery required by the non-blocking case.
*/
public static final boolean BLOCKING_OSID_LOAD = false;
/* states for DataSourceListener callbacks */
public static final String DS_UNMARSHALLED = "DS_UNMARSHALLED";
public static final String DS_CONFIGURED = "DS_CONFIGURED";
public static final String DS_ADDED = "DS_ADDED";
public static final String DS_ERROR = "DS_ERROR";
public static final String DS_ALL_CONFIGURED = "DS_ALL_CONFIGURED";
public static final String DS_SAVING = "DS_SAVING";
protected static final String PASS = "pass"; // a key that cotains this string is encrypted.
private static final File userFolder = tufts.vue.VueUtil.getDefaultUserFolder();
private static final String xmlFilename = userFolder.getAbsolutePath() + "/" + tufts.vue.VueResources.getString("dataSourceSaveToXmlFilename");
private static final VueDataSourceManager singleton = new VueDataSourceManager(true);
/**
* This set will only maintain uniqueness based on the hashCode, which is currently
* defaulting to the System.identityHashCode for known implemented data sources
* (VueDataSource's). This is fine for our purposes now, tho a more complete impl
* would require DataSource.hashCode() to return the getId().getIdString() (and then
* we could also use a HashMap for referencing by ID string).
*
* [ Old as of 12/22/08: We need a set because upon creation, VueDataSources always
* attempt to add themselves to the global data source list (in setDone, called by
* castor during unmarshalling). Don't know if we need that behavior, but this impl
* allows that to happen w/out putting duplicates in the global list. ]
*
* -- SMF 10/2007
*/
private final Set<DataSource> DataSources;
/** This is only used marshalling and unmarshalling */
private final Vector<DataSource> marshallingVector = new Vector();
private final List<edu.tufts.vue.dsm.DataSourceListener> dataSourceListeners
= new java.util.concurrent.CopyOnWriteArrayList<edu.tufts.vue.dsm.DataSourceListener>();
private volatile boolean isMarshalling;
private boolean isLoaded;
// Todo: this class doesn't always appear to be threadsafe.
// Do we we really want getInstance to return an empty default DSM
// instance, that changes after load is called? Currently this
// doesn't matter, VDSM has no member variables -- all are static,
// but it isn't a very clean way to do it.
public static VueDataSourceManager getInstance() {
return singleton;
}
/** this is public for castor persistance support only */
public VueDataSourceManager() {
isMarshalling = true;
DataSources = null; // so will deliberately NPE if use is attempted
}
private VueDataSourceManager(boolean isSingleton) {
DataSources = new LinkedHashSet();
}
public synchronized void save() {
// why do we notify on save? keeping this only in case something is
// currently depending on this...
notifyDataSourceListeners(DS_SAVING);
marshall(new File(this.xmlFilename));
}
public synchronized void reload() {
isLoaded = false;
load();
}
public synchronized void load() {
if (isLoaded)
return;
try {
// if (true) throw new Error("oh my"); // exception test
final File file = new File(xmlFilename);
if (file.exists()) {
DataSources.clear();
final VueDataSourceManager unmarshalled = unMarshalVDSM(file);
// currently, we really only need the list of DataSources from
// the unmarshalled VDSM instance -- we'd could just marshall
// a list of items. We throw away the newly marshalled
// VDSM and copy the result into our singleton, so that
// it can be a final constant initialized at class load.
DataSources.addAll(unmarshalled.marshallingVector);
isLoaded = true;
if (BLOCKING_OSID_LOAD) {
for (edu.tufts.vue.dsm.DataSource ds : DataSources) {
try {
if (ds instanceof VueDataSource)
((VueDataSource)ds).assignRepositoryConfiguration();
} catch (Throwable t) {
Log.error(t);
}
}
// they're already fully configured at this point
notifyDataSourceListeners(DS_ALL_CONFIGURED);
} else {
notifyDataSourceListeners(DS_UNMARSHALLED);
// DS_ALL_CONFIGURED will come later
}
} else {
debug("Installed DataSources not found; missing " + file);
}
} catch (Throwable t) {
Log.warn("unmarshalling;", t);
throw Util.wrapException("unmarshalling", t);
}
}
/**
* Assign repository configurations to loaded DataSources. Currently only handles
* VueDataSource impls. This will spawn a thread for each assignment, as the
* RepositoryManager implementation for each source can possibly hang while
* assigning configuration, and there's no need to hang the whole application during
* startup for a single hung DataSource / Repository failing to respond.
*
* @param clientUI -- if non-null, repaint() will be called on this each time a
* DataSource completes configuration as a very simple callback, in addition to
* notifications to any DataSourceListeners
*/
public synchronized void startRepositoryConfiguration(final java.awt.Component clientUI) {
Log.info("configuring data sources; n=" + DataSources.size());
final edu.tufts.vue.dsm.DataSource dataSources[] = getDataSources();
final java.util.concurrent.atomic.AtomicInteger RunCount =
new java.util.concurrent.atomic.AtomicInteger(dataSources.length);
for (final edu.tufts.vue.dsm.DataSource ds : dataSources) {
if (ds instanceof VueDataSource == false) {
Log.warn("unhandled DataSource impl, cannot configure: " + Util.tags(ds));
RunCount.decrementAndGet();
}
}
for (final edu.tufts.vue.dsm.DataSource ds : dataSources) {
if (ds instanceof VueDataSource == false)
continue;
Log.debug("configuring " + ds);
// final String threadName = String.format("%s@%08x %8.8s",
// ds.getClass().getSimpleName(),
// System.identityHashCode(ds),
// ds.getRepositoryDisplayName()))
final String threadName = String.format("conf:ds@%08x", System.identityHashCode(ds));
new Thread(threadName) {
@Override
public void run() {
try {
try {
//if (ds.getRepositoryDisplayName().startsWith("(Conn"))
//try { Thread.sleep(5000); } catch (Throwable t) {}
((VueDataSource)ds).assignRepositoryConfiguration();
if (tufts.vue.DEBUG.DR)
Log.info(String.format("configured %-33s %s",
'"' + ds.getRepositoryDisplayName() + '"',
VueDataSource.idString(ds.getRepositoryId())));
Log.info(String.format("configured %-33s %s",
'"' + ds.getRepositoryDisplayName() + '"',
ds.getRepository()));
//getRepositoryID(ds);
if (ds.getRepository() == null)
notifyDataSourceListeners(DS_ERROR, ds);
else
notifyDataSourceListeners(DS_CONFIGURED, ds);
} catch (Throwable t) {
Log.error("configuration error: " + Util.tags(ds), t);
notifyDataSourceListeners(DS_ERROR, ds);
}
// TODO: VueDataSource accessors accessed during client painting
// (in AWT thread) or are not fully thread-safe against the
// above assignRepositoryConfiguration in this config thread.
if (clientUI != null)
clientUI.repaint();
//else Log.debug("config complete, no UI to update");
} catch (Throwable t) {
Log.error("configuring", t);
} finally {
if (RunCount.decrementAndGet() <= 0) {
// Whichever thread finishes last will deliver the DS_ALL_CONFIGURED
// event. If any thread should hang, this will never be delivered, so
// listeners should ideally be designed to provide at least some
// functionality based only on the delivery of the DS_CONFIGURED events
// as they come in.
setName(getName().toUpperCase()); // tweak the thread name for debug
notifyDataSourceListeners(DS_ALL_CONFIGURED);
}
}
}
}.start();
}
}
/**
* Return the list of found DataSources. Will trigger a blocking file I/O load if none are
* present. Some or all of the returned DataSources may be in an "unconfigured" state (without
* a repository), requiring a later call to startRepositoryConfiguration before they can be
* used.
*/
public synchronized edu.tufts.vue.dsm.DataSource[] getDataSources() {
if (!isLoaded)
load();
return Util.toArray(DataSources, DataSource.class);
}
/**
* Add the given data source to the global list if it isn't already there.
* Will immediately save if it was added.
*/
public synchronized void add(edu.tufts.vue.dsm.DataSource ds) {
if (DataSources.add(ds)) {
Log.info("add data src: " + ds);
notifyDataSourceListeners(DS_CONFIGURED, ds);
if (!isMarshalling)
save();
}
}
/**
* Add the given data source to the global list if it's there (any matching the id, tho there should be only one).
* Will immediately save if anything was removed.
*/
public void remove(org.osid.shared.Id id) {
if (id == null) {
Util.printStackTrace(this + ".remove: null id");
return;
}
synchronized (this) {
boolean removed = false;
try {
Iterator<DataSource> i = DataSources.iterator();
while (i.hasNext()) {
DataSource ds = i.next();
if (id.isEqual(ds.getId())) {
i.remove();
if (removed)
Log.warn("removed again: " + ds);
else
Log.info("removed: " + ds);
removed = true;
// should be able to break, but just in case different instances have same ID..
}
}
} catch (Throwable t) {
Log.error("remove " + Util.tags(id), t);
}
if (removed && !isMarshalling)
save();
}
}
/**
*/
public edu.tufts.vue.dsm.DataSource getDataSource(org.osid.shared.Id dataSourceId) {
try {
synchronized (this) {
for (DataSource ds : DataSources)
if (dataSourceId.isEqual(ds.getId()))
return ds;
}
} catch (Throwable t) {
Log.warn("found no DataSource with id " + Util.tags(dataSourceId));
}
return null;
}
/**
*/
public org.osid.repository.Repository[] getIncludedRepositories() {
final List<Repository> included = new ArrayList();
synchronized (this) {
for (DataSource ds : DataSources)
if (ds.isIncludedInSearch()) {
Repository r = ds.getRepository();
if (r == null) {
if (tufts.vue.DEBUG.DR) Log.debug("has no repository; not included: " + ds);
} else
included.add(r);
}
}
return Util.toArray(included, Repository.class);
// java.util.Vector results = new java.util.Vector();
// int size = marshallingVector.size();
// for (int i=0; i < size; i++) {
// edu.tufts.vue.dsm.DataSource ds = (edu.tufts.vue.dsm.DataSource)marshallingVector.elementAt(i);
// if (ds.isIncludedInSearch()) {
// try {
// debug("Getting included data sourceA " + tufts.Util.tag(ds));
// debug("Getting included data sourceB " + tufts.Util.tags(ds.getId()));
// debug("Getting included data source0 " + ds.getId().getIdString());
// debug("Getting included data source1 " + ds.getRepository());
// debug("Getting included data source2 " + ds.getRepository().getDisplayName());
// debug("Getting included data source3 " + ds.getRepository().getId().getIdString());
// } catch (Throwable t) {
// }
// results.addElement(ds.getRepository());
// }
// }
// size = results.size();
// org.osid.repository.Repository repositories[] = new org.osid.repository.Repository[size];
// for (int i=0; i < size; i++) {
// repositories[i] = (org.osid.repository.Repository)results.elementAt(i);
// }
// return repositories;
}
private static void debug(String s) {
Log.info(s);
}
public edu.tufts.vue.dsm.DataSource[] getIncludedDataSources() {
final List<DataSource> included = new ArrayList();
synchronized (this) {
for (DataSource ds : DataSources)
if (ds.isIncludedInSearch())
included.add(ds);
}
return Util.toArray(included, DataSource.class);
}
/**
*/
public java.awt.Image getImageForRepositoryType(org.osid.shared.Type repositoryType) {
return null;
}
/**
*/
public java.awt.Image getImageForSearchType(org.osid.shared.Type searchType) {
return null;
}
/**
*/
public java.awt.Image getImageForAssetType(org.osid.shared.Type assetType) {
return null;
}
/** for castor persistance only -- should not be used for fetching the data source list
* This will normally return null except during marshalling */
public Vector getDataSourceVector() {
return isMarshalling ? marshallingVector : null;
}
// public void setDataSourceVector(Vector dsv) {
// marshallingVector = dsv;
// }
public void addDataSourceListener(edu.tufts.vue.dsm.DataSourceListener listener) {
// threadsafe: is CopyOnWriteArrayList
dataSourceListeners.add(listener);
}
public void removeDataSourceListener(edu.tufts.vue.dsm.DataSourceListener listener) {
// threadsafe: is CopyOnWriteArrayList
dataSourceListeners.remove(listener);
}
public void notifyDataSourceListeners(Object state) {
notifyDataSourceListeners(state, null);
}
public synchronized void notifyDataSourceListeners(Object state, DataSource changed) {
// synchronized so notifications from different threads are at least atomic
// to the state of all DataSources for each notification batch
final edu.tufts.vue.dsm.DataSource dataSources[] = getDataSources();
for (edu.tufts.vue.dsm.DataSourceListener listener : dataSourceListeners) {
try {
listener.changed(dataSources, state, changed);
} catch (Throwable t) {
Log.error("DataSourceListener failure: " + Util.tags(listener), t);
}
}
}
private synchronized void marshall(File file)
{
//System.out.println("Marshalling: file -"+ file.getAbsolutePath());
marshallingVector.clear();
marshallingVector.addAll(DataSources);
for (DataSource ds : marshallingVector) {
try {
Log.info("marshalling: " + ds + "; RepositoryID=" + getRepositoryID(ds));
} catch (Throwable t) {
Log.warn("Marshalling:", t);
}
}
isMarshalling = true;
try {
Mapping mapping = tufts.vue.action.ActionUtil.getDefaultMapping();
FileWriter writer = new FileWriter(file);
Marshaller marshaller = new Marshaller(writer);
marshaller.setEncoding("US-ASCII");
marshaller.setMapping(mapping);
marshaller.setMarshalListener(new PropertyEntryMarshalListener());
Log.debug("Marshalling to " + file + "...");
marshaller.marshal(this);
writer.flush();
writer.close();
Log.info(" Marshalled to " + file);
} catch (Throwable t) {
Log.error("marshall to " + file, t);
} finally {
isMarshalling = false;
}
}
public static String getRepositoryID(DataSource ds) {
final Repository r = ds.getRepository();
String id = "<null-repository>";
if (r != null) {
try {
id = r.getId().getIdString();
} catch (Throwable t) {
id = "<repositoryID:" + t + ">";
}
}
return id;
}
final static String CRIMSON_ENCODING_EXCEPTION_PREFIX = "Declared encoding \"";
private static synchronized VueDataSourceManager unMarshalVDSM(File file)
throws java.io.IOException,
org.exolab.castor.xml.MarshalException,
org.exolab.castor.xml.ValidationException,
org.exolab.castor.mapping.MappingException
{
VueDataSourceManager vdsm = null;
try {
vdsm = unmarshalWithEncoding(file, null);
} catch (org.exolab.castor.xml.MarshalException e) {
Log.error(e.toString() + "; " + file);
if (e.getCause() instanceof org.xml.sax.SAXParseException &&
e.getMessage() != null &&
e.getMessage().startsWith(CRIMSON_ENCODING_EXCEPTION_PREFIX))
{
// We are getting BOGUS, yet unavoidable SAXParseExceptions, even if the
// xml tag declared encoding is UTF-8, and the java stream encoding is
// UTF-8, because the java internal name is "UTF8" w/out the dash, and
// that won't match! This is coming from the Apache Crimson XML parser
// -- did the default parser change on us at some point? Via a class library
// order change?
// For more see:
// http://www.experts-exchange.com/Programming/Languages/Java/Q_24185321.html
// We've turned off the Crimson parser for now, restoring Xerces, which
// has made this exception go away.
String declared = null;
try {
declared = e.getMessage().substring(CRIMSON_ENCODING_EXCEPTION_PREFIX.length());
declared = declared.substring(0, declared.indexOf('"'));
Log.info(String.format("Attempting alternate encoding [%s]", declared));
} catch (Throwable t) {
Log.error(t);
throw e;
}
vdsm = unmarshalWithEncoding(file, declared);
}
}
for (DataSource ds : vdsm.marshallingVector) {
try {
Log.info("unmarshalled: " + ds);
} catch (Throwable t) {
Log.warn(t);
}
}
Log.debug("unmarshall: done.");
return vdsm;
}
private static VueDataSourceManager unmarshalWithEncoding(File file, String encoding)
throws java.io.IOException,
org.exolab.castor.xml.MarshalException,
org.exolab.castor.xml.ValidationException,
org.exolab.castor.mapping.MappingException
{
Log.info("Unmarshalling: " + file + "; encoding=[" + encoding + "]");
final Unmarshaller unmarshaller = tufts.vue.action.ActionUtil.getDefaultUnmarshaller(file.toString());
unmarshaller.setUnmarshalListener(new PropertyEntryUnMarshalListener());
//final FileReader reader = new FileReader(file);
final InputStreamReader reader;
if (encoding != null)
reader = new InputStreamReader(new FileInputStream(file), encoding);
else
reader = new InputStreamReader(new FileInputStream(file));
Log.debug(String.format("reader actual encoding name: [%s]", reader.getEncoding()));
final InputSource inputSource = new InputSource(reader);
if (encoding != null) inputSource.setEncoding(encoding); // don't think required but just in case
final VueDataSourceManager vdsm = (VueDataSourceManager) unmarshaller.unmarshal(inputSource);
reader.close();
return vdsm;
}
}
class PropertyEntryMarshalListener implements org.exolab.castor.xml.MarshalListener {
public boolean preMarshal(java.lang.Object object) {
if(object instanceof tufts.vue.PropertyEntry) {
tufts.vue.PropertyEntry pe = (tufts.vue.PropertyEntry) object;
if(pe.getEntryKey().toLowerCase().contains(VueDataSourceManager.PASS)) {
pe.setEntryValue(edu.tufts.vue.util.Encryption.encrypt(pe.getEntryValue().toString()));
}
}
return true;
}
public void postMarshal(java.lang.Object object) {
if(object instanceof tufts.vue.PropertyEntry) {
tufts.vue.PropertyEntry pe = (tufts.vue.PropertyEntry) object;
if(pe.getEntryKey().toLowerCase().contains(VueDataSourceManager.PASS)) {
pe.setEntryValue(edu.tufts.vue.util.Encryption.decrypt(pe.getEntryValue().toString()));
}
}
}
}
class PropertyEntryUnMarshalListener implements org.exolab.castor.xml.UnmarshalListener {
public void unmarshalled(java.lang.Object object) {
if(object instanceof tufts.vue.PropertyEntry) {
tufts.vue.PropertyEntry pe = (tufts.vue.PropertyEntry) object;
if(pe.getEntryKey().toLowerCase().contains(VueDataSourceManager.PASS)) {
pe.setEntryValue(edu.tufts.vue.util.Encryption.decrypt(pe.getEntryValue().toString()));
}
}
}
public void attributesProcessed(java.lang.Object object) {
}
public void fieldAdded(java.lang.String fieldName, java.lang.Object parent, java.lang.Object child) {
}
public void initialized(java.lang.Object object) {
}
}