/*
* 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 tufts.vue.ds;
import tufts.Util;
import tufts.vue.DEBUG;
import tufts.vue.MetaMap;
import java.util.*;
import java.io.*;
import java.net.URL;
import tufts.vue.Resource;
import tufts.vue.LWComponent;
import org.xml.sax.InputSource;
import bsh.EvalError;
import bsh.Interpreter;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
/**
* A Schema is combination data definition, and when loaded with data, serves as a
* minature, searchable data-base with automatic up-front analyitics. Its "rows" of
* data are all key-value paired, so not all rows have to contain all fields, which is
* important for treating repeated XML fragments as "rows" (e.g., RSS feed items).
* The data analysis performed looks at each column (Field) and if the values are
* generally "short" enough, it will enumerate all the unique values found in that
* column.
*
* @version $Revision: 1.54 $ / $Date: 2010-02-03 19:13:16 $ / $Author: mike $
* @author Scott Fraize
*/
public class Schema implements tufts.vue.XMLUnmarshalListener {
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(Schema.class);
/** A map of all Fields from all schemas for auto-discovering associations based on the field name */
// private static final Multimap<String,Field> AllFieldsByLowerName = Multimaps.synchronizedMultimap
// ((Multimap<String,Field>)Multimaps.newHashMultimap()); // WTF? Strange javac complaint...
private static final Multimap<String,Field> AllFieldsByLowerName = Multimaps.newHashMultimap();
/** All possible column's in this Schema */
private final Map<String,Field> mFields = new HashMap();
//private final Map<String,Field> mFields = new LinkedHashMap();
/** Ordered list of column's in this Schema -- same contents as mFields */
private final List<Field> mFieldList = new ArrayList();
private final Collection<Field> mPersistFields = new ArrayList();
private boolean mXMLRestoreUnderway;
private Field mKeyField;
private Field mEncodingField;
private final List<DataRow> mRows = new ArrayList();
//private Object mSource;
private Resource mResource;
protected int mLongestFieldName = 10; // for debug
private String mName;
private Field mImageField;
private LWComponent mStyleNode;
private String GUID;
private String DSGUID;
private boolean isUserStyled;
private boolean isDiscarded;
boolean mKeyFold;
// /** construct an empty schema */
// public Schema() {}
/** id used for within-map reference for persisting the relationship between a MetaMap and a schema */
private final String mLocalID;
private int mContextRowNodeCount;
private static final java.util.concurrent.atomic.AtomicInteger NextLocalId = new java.util.concurrent.atomic.AtomicInteger();
///** map of locations/addresses to schema instances */
//private static final Map<String,Schema> SchemaMap = new java.util.concurrent.ConcurrentHashMap();
///** contains "empty" (no data) schemas, which are retained as handles to be replaced if actual data schemas arrive */
//private static final Collection<Schema> SchemaHandles = Collections.synchronizedList(new ArrayList());
private static final Map<Resource,Schema> _SchemaByResource = Collections.synchronizedMap(new HashMap());
private static final Map<String,Schema> _SchemaByDSGUID = Collections.synchronizedMap(new HashMap());
private static Schema newAuthorityInstance(Resource r, String dsGUID) {
final Schema s = new Schema();
s.setResource(r);
s.setDSGUID(dsGUID);
s.setGUID(edu.tufts.vue.util.GUID.generate());
//if (DEBUG.SCHEMA) Log.debug("INSTANCED SCHEMA " + s + "\n");
if (DEBUG.SCHEMA) Log.debug(s + "; INSTANCED");
registerAsAuthority(s);
// would we want to wait to do registrations until we actually load the rows?
if (DEBUG.SCHEMA) Log.debug(s + "; REGISTERED");
if (DEBUG.SCHEMA) dumpAuthorities();
return s;
}
// TODO: something like this would probably make all sorts of stuff so much easier,
// tho we'd have to re-check tons of code. Would help with nodes that have
// "discarded" schema references in the Undo queue or cut/paste buffer, as
// well as associations Fields (we'd need a similar scheme for Fields).
// @Override public boolean equals(Object o) {
// return o instanceof Schema && ((Schema)o).DSGUID.equals(DSGUID);
// }
// We could establish fast similiarity based on the following rules:
// If the the FILE exists (Resource, but what if a network source?)
// Then each existing file gets an absolute unique ID, overriding
// DSGUID. If the file no longer exists, then map missing files
// by found DSGUID's. Tho, what if a user changes the file in a DSGUID,
// and then expects prior maps pointing to that DSGUID to use the
// new file? So actually, DSGUID does neet to take priority.
// public boolean same(Schema s) {
// return s != null && s.mContentID == this.mContentID;
// }
public static Set<Map.Entry<String,Schema>> getAllByDSGUID() {
return _SchemaByDSGUID.entrySet();
}
public static Set<Map.Entry<Resource,Schema>> getAllByResource() {
return _SchemaByResource.entrySet();
}
private static synchronized void registerAsAuthority(Schema schema) {
if (schema.DSGUID != null)
registerByDSGUID(schema, schema.DSGUID);
registerByResource(schema, schema.getResource());
}
private static synchronized void registerByDSGUID(Schema s, String dsGUID) {
Schema prev = _SchemaByDSGUID.put(dsGUID, s);
if (prev != null && prev != s) {
Log.warn("REPLACING EXISTING DSGUID; orphaning: " + prev, new Throwable("HERE"));
}
if (prev == s)
Log.info(s + "; was already AUTHORITATIVE for DSGUID " + dsGUID);
else
Log.info(s + "; recorded as AUTHORITATIVE for DSGUID " + dsGUID);
if (s.getDSGUID() != dsGUID)
Log.warn(""+s, new Throwable("INCONSISTENT DSGUID STATE; " + dsGUID));
}
private static synchronized void registerByResource(Schema s, Resource r) {
Schema prev = _SchemaByResource.put(r, s);
if (prev != null) {
Log.warn("REPLACING EXISTING byRESOURCE; orphaning" + prev, new Throwable("HERE"));
}
if (prev == s)
Log.info(s + "; was already AUTHORITATIVE for Resource: " + r);
else
Log.info(s + "; recorded as AUTHORITATIVE for Resource: " + r);
if (s.getResource() != r)
Log.warn(""+s, new Throwable("INCONSISTENT RESOURCE STATE " + r + " != " + s.getResource()));
}
private synchronized void trackUserStyles(Schema s) {
if (!isUserStyled) {
copyStyles(s);
isUserStyled = true;
}
}
private void copyStyles(Schema s) {
if (s.mPersistFields.size() > 0) {
if (DEBUG.SCHEMA) Log.debug("copyStyles; from persisted handles:"
+ "\n\tsource: " + s
+ "\n\ttarget: " + this);
copyStyles(s.mPersistFields);
} else {
if (DEBUG.SCHEMA) Log.debug("copyStyles; from live Fields:"
+ "\n\tsource: " + s
+ "\n\ttarget: " + this);
copyStyles(s.mFieldList);
}
setRowNodeStyle(s.getRowNodeStyle());
}
private void copyStyles(Collection<Field> fields) {
for (Field src : fields) {
Field target = getField(src.getName());
if (target != null)
target.setStyleNode(runtimeInitStyleNode(src.getStyleNode()));
}
}
/** mark this Schema as discarded.
* @param authority the authoritative Schema this one is being discard for -- if
* it hasn't been user styled yet, it will be user-styled based on this Schema
* (the one being discarded).
*/
private synchronized void discardFor(Schema authority) {
if (!isDiscarded) {
if (DEBUG.SCHEMA) Log.info(this + ": DISCARDED");
isDiscarded = true;
authority.trackUserStyles(this);
}
}
synchronized boolean isDiscarded() {
return isDiscarded;
}
public synchronized static void dumpAuthorities() {
Log.debug("Current VUE authoritative schemas by DSGUID:");
//Util.dump(Schema.getAllByDSGUID());
Util.dump(_SchemaByDSGUID, "BY-DSGUID");
Log.debug("Current VUE authoritative schemas by RESOURCE:");
//Util.dump(Schema.getAllByResource());
Util.dump(_SchemaByResource, "BY-RESOURCE");
}
private static final boolean SCHEMA_AUTHORITY = true;
private static final boolean SCHEMA_REFERENCE = false;
public static Schema getInstance(Resource r, String dsGUID) {
return getInstance(r, dsGUID, null, SCHEMA_REFERENCE);
}
public static Schema getNewAuthorityInstance(Resource r, String dsGUID, String displayName) {
return getInstance(r, dsGUID, displayName, SCHEMA_AUTHORITY);
}
private static synchronized Schema getInstance
(Resource r,
String dsGUID,
String displayName,
boolean infoIsAuthority)
{
if (true) {
return getInstanceAuthoritiesAreNew(r, dsGUID, displayName, infoIsAuthority);
} else {
return getInstanceAuthoritiesAreReloaded(r, dsGUID, displayName, infoIsAuthority);
}
}
private static Schema getInstanceAuthoritiesAreNew
(Resource r,
String dsGUID,
String displayName,
boolean infoIsAuthority)
{
if (infoIsAuthority) {
return newAuthorityInstance(r, dsGUID);
} else {
return lookup(dsGUID, r, displayName);
}
}
/** If an existing schema exists matching the given data, return that, otherwise, a new schema instance */
// Not ideal: it's safer to guarante a new fresh Schema instance here
// every time we have authoritative information and to only copy FIELD
// STYLE info over if there is an existing matching schema.
private static Schema getInstanceAuthoritiesAreReloaded
(Resource r,
String dsGUID,
String displayName,
boolean infoIsAuthority)
{
Schema s = lookup(dsGUID, r, displayName);
if (s == null) {
return newAuthorityInstance(r, dsGUID);
} else {
if (infoIsAuthority) {
// this is the codepath from XmlDataSource to load the actual DataSource schema, which has the
// ultime authority info -- if we're keeping an existing schema, it's only for the styles.
if (!r.equals(s.getResource())) {
Log.info("updating Resource in authority schema:"
+ "\n\told: " + s.getResource()
+ "\n\tnew: " + r);
s.setResource(r);
}
if (dsGUID != null && !dsGUID.equals(s.getDSGUID())) {
Log.info("updating DSGUID in authority schema", new Throwable("HERE"));
s.setDSGUID(dsGUID);
}
if (displayName != null && !displayName.equals(s.mName)) {
Log.info("updating Name in authority schema");
s.setName(displayName);
}
// re-register in case Resource or DSGUID has changed -- not this could allow
// multiple Resource's / DSGUID's to map to the same Schema, which is fine.
registerAsAuthority(s);
}
return s;
}
}
private static synchronized Schema lookup
(final String dsGUID,
final Resource r,
final String name)
{
Schema s = _SchemaByDSGUID.get(dsGUID);
if (s == null) {
s = _SchemaByResource.get(r);
if (s != null) {
// actually, looking by Resource may not be correct: the same resource could
// be used with a differet configuration (esp XML, tho could apply to CSV as well),
// generating schema's that are actually different.
if (DEBUG.SCHEMA) Log.debug("lookup: found by RESOURCE " + s + "; " + r);
}
} else {
if (DEBUG.SCHEMA&&DEBUG.META) {
Log.debug("lookup: found by DSGUID " + s);
// Log.debug("fetch: existing fields:");
// Util.dump(s.mFields);
}
}
//if (DEBUG.SCHEMA) Log.debug("lookup: returning " + s);
return s;
}
private static Schema lookup(Schema s)
{
synchronized (s) {
return lookup(s.getDSGUID(), s.getResource(), s.getName());
}
}
/** for looking up a loaded (data-containing) schema from a an empty schema-handle.
* If the given schema is already loaded, or if no matching loaded schema can be found,
* the passed in schema is returned.
*/
public static Schema lookupAuthority(final Schema schema) {
if (DEBUG.SCHEMA && DEBUG.META) Log.debug("LOOKUP SCHEMA " + schema);
if (schema == null)
return null;
// if (schema.isLoaded())
// return schema;
final Schema authoritativeSchema = lookup(schema);
if (authoritativeSchema != null && authoritativeSchema != schema) {
if (schema.isLoaded() && DEBUG.Enabled) {
Util.printStackTrace("replacing loaded schema " + schema + " with authority " + authoritativeSchema);
}
return authoritativeSchema;
}
else
return schema;
}
/** interface {@link XMLUnmarshalListener} -- track us */
public synchronized void XML_completed(Object context) {
// As the resource isn't in the LWComponent hierarchy, it won't be updated in
// the map. TODO: too bad actually -- then it could make use of the
// relative-to-map file path code -- would be a good idea to move these to the
// map.
((tufts.vue.URLResource)getResource()).XML_completed("SCHEMA-MANUAL-INIT");
this.DSGUID = getResource().getProperty("@DSGUID");
for (Field field : mPersistFields) {
field.setSchema(this);
if (DEBUG.SCHEMA) Log.debug(String.format("seen field %s%-20s%s",
Util.TERM_YELLOW, field, Util.TERM_CLEAR));
}
// We can't do this now, at deserialize time, as we wont see any Schema's
// that have yet to deserialize.
// createAttachableAssociations(mPersistFields);
mXMLRestoreUnderway = false;
if (DEBUG.Enabled) Log.debug(this + " RESTORED; DSGUID=" + DSGUID + "\n");
}
/**
* Any deserialized Schema's saved with the map that match any already loaded
* authoritative Schema's will be replaced in the set of restored nodes, and and all
* Field's found will be scanned for associations to be added.
*/
public static synchronized void restoreSavedMapSchemas
(final tufts.vue.LWMap restoredMap,
final Collection<Schema> restoredSchemaHandles,
final Collection<LWComponent> allRestored)
{
if (DEBUG.SCHEMA) {
Log.debug("SCHEMA's in map " + restoredMap + ":");
Util.dump(restoredSchemaHandles);
Schema.dumpAuthorities();
}
// FIRST attempt all authority replacements:
for (Schema schema : restoredSchemaHandles) {
try {
if (DEBUG.SCHEMA) Log.debug(schema + "; RESTORE AND REPLACE");
schema.restoreFields();
updateRestoredMapToAuthorities(schema, restoredMap, allRestored);
} catch (Throwable t) {
Log.error("error updating schema references for " + schema, t);
}
}
// THEN scan for associations, so we can create associations based on all new authoritative references:
for (Schema schema : restoredSchemaHandles) {
try {
//if (DEBUG.SCHEMA) Log.debug(schema + "; CREATE ASSOCIATIONS");
createAttachableAssociations(schema, schema.mPersistFields); // note that Fields may have just been discarded
} catch (Throwable t) {
Log.error("error scanning for associations in " + schema, t);
}
}
}
private synchronized void restoreFields() {
for (Field f : mPersistFields) {
final LWComponent style = f.getStyleNode();
if (DEBUG.SCHEMA) Log.debug(String.format("restore field %s%-23s%s style=%s",
Util.TERM_GREEN, f, Util.TERM_CLEAR, style));
runtimeInitStyleNode(style);
addField(f); // must do this to get all field names in the global multi-map for association lookups
}
}
private static synchronized boolean updateRestoredMapToAuthorities
(final Schema handle,
final tufts.vue.LWMap restoredMap,
final Collection<LWComponent> allRestored)
{
Schema existing = lookup(handle);
if (existing != null) {
if (DEBUG.Enabled) Log.debug("DUMPING THIS SCHEMA FOR EXISTING:\n\t dumped: " + handle + "\n\texisting: " + existing);
handle.discardFor(existing);
replaceSchemaReferences(handle, existing, allRestored, restoredMap);
// HOLY CRAP: we also need to update association Field references!
// As currently an association can only be USED once the data-source is loaded,
// lets just freakin NOT scan for associations until the actual data-source itself is loaded?
// Or do we first want to see what it would take to actually turn associations on and off?
// For assoc replacement keep it simple: be able to re-scan associations and create new ones
// whenever a new authoritative schema is identified, and when added as a new association,
// always scan and dump any associations that include references to orphaned schema's
// (which will need to be marked as such).
return true;
} else
return false;
}
private static synchronized void replaceSchemaReferences
(final Schema oldSchema,
final Schema newSchema,
final Collection<LWComponent> nodes,
final tufts.vue.LWMap sourceMap)
{
int updateCount = 0;
//newSchema.setRowNodeStyle(oldSchema.getRowNodeStyle()); // ideally, only when the existing isn't already used anywhere
for (LWComponent c : nodes) {
final MetaMap data = c.getRawData();
if (data == null)
continue;
if (data.getSchema() == oldSchema) {
data.setSchema(newSchema);
updateCount++;
}
// else if (DEBUG.Enabled) {
// Log.debug("keeping schema: " + data.getSchema());
// }
}
Log.info(String.format("updated %d schema handle references in %s", updateCount, sourceMap));
}
// codepath: called in XmlDataSource anytime we load a new authoritative, "real" schema -- as such
// couldn't we do this automatically?
public static synchronized void reportNewAuthoritativeSchema
(final Schema newAuthority,
final Collection<tufts.vue.LWMap> maps)
{
tufts.vue.gui.GUI.invokeOnEDT(new Runnable() { public void run() {
// safest to ensure all this happs on the AWT thread, so later association data
// fetches (data search & filter actions) don't need synchronized read access
makeSchemaReferencesAuthoritative(newAuthority, maps);
Association.updateForNewAuthoritativeSchema(newAuthority);
}});
}
/** find all schema handles in all nodes that match the new schema
* and replace them with pointers to the live data schema */
private static synchronized void makeSchemaReferencesAuthoritative
(final Schema newAuthority,
final Collection<tufts.vue.LWMap> maps)
{
if (DEBUG.Enabled) {
Log.debug("makeSchemaReferencesAuthoritative; " + newAuthority + "; maps: " + maps);
dumpAuthorities();
}
if (!newAuthority.isLoaded()) {
Log.warn("newly loaded schema is empty: " + newAuthority, new Throwable("FYI"));
return;
}
int scanCount = 0;
int scanMapCount = 0;
int updateCount = 0;
int updateMapCount = 0;
// todo: if this ever gets slow, could improve performance by pre-computing a
// lookup map of all schema handles that map to the new schema (which will
// usually contain only a single schema mapping) then we only have to check
// every schema reference found against the pre-computed lookups instead of
// doing a Schema.lookup against all loaded Schema's. E.g., we'd make a
// a series of calls to replaceSchemaReferences.
for (tufts.vue.LWMap map : maps) {
final int countAtMapStart = updateCount;
final Collection<LWComponent> nodes = map.getAllDescendents(LWComponent.ChildKind.ANY);
for (LWComponent c : nodes) {
final MetaMap data = c.getRawData();
if (data == null)
continue;
final Schema curSchema = data.getSchema();
if (curSchema != null) {
final Schema newSchema = Schema.lookupAuthority(curSchema);
// Log.debug("curs: " + newSchema);
// Log.debug("auth: " + newSchema);
scanCount++;
if (newSchema != curSchema) {
curSchema.discardFor(newSchema);
data.setSchema(newSchema);
updateCount++;
if (newSchema != newAuthority) {
Log.warn("out of date schema in " + c + "; oldSchema=" + curSchema + "; replaced with " + newSchema);
} else {
//if (DEBUG.SCHEMA) Log.debug("replaced schema handle in " + c);
}
}
}
}
if (updateCount > countAtMapStart)
updateMapCount++;
scanMapCount++;
}
Log.info(String.format("scanned %d schema references in %d maps", scanCount, scanMapCount));
Log.info(String.format("replaced %d schema references in %d maps", updateCount, updateMapCount));
}
private static synchronized void createAttachableAssociations(Schema schema, Collection<Field> restoredFields) {
if (DEBUG.SCHEMA) Log.debug(schema + "; createAttachableAssociations");
for (final Field field : restoredFields) {
for (Field.PersistRef ref : field.getRelatedFieldRefs()) {
if (DEBUG.SCHEMA) Log.debug(schema + "; persisted relation for " + field + ": " + ref);
for (Field possibleMatch : AllFieldsByLowerName.get(ref.fieldName.toLowerCase())) {
final String guid = possibleMatch.getSchema().getGUID();
//Log.debug("Checking GUID " + guid);
if (ref.schemaGuid.equals(guid)) {
final Field match = possibleMatch;
if (match.getSchema().isDiscarded()) {
Log.debug("ignoring association match for discarded schema: " + match);
} else {
Log.debug("found live field to match ref: " + ref + " = " + Util.tags(match));
//tufts.vue.gui.GUI.invokeOnEDT(new Runnable() { public void run() {
// safest to ensure all this happs on the AWT thread, so later association data
// fetches (data search & filter actions) don't need synchronized read access
Association.add(field, match);
// }});
}
}
}
}
}
}
//private static tufts.vue.LWContainer FalseStyleParent = new tufts.vue.LWNode("FalseStyleParent");
private Field addField(final Field newField) {
return addField(newField, null, false);
}
private synchronized Field addField(final Field newField, final Field nearField, boolean before)
{
newField.setSchema(this);
// note: for new Fields, constructor may have trimmed the name,
// so we should always fetch via f.getName() just in case.
// (instead of ever passing in the name of the new field)
final String name = newField.getName();
mFields.put(name, newField);
if (nearField != null)
mFieldList.add(mFieldList.indexOf(nearField) + (before?0:1), newField);
else
mFieldList.add(newField);
final String keyName = name.toLowerCase();
// This will auto-add associations for fields of the same name -- too aggressive tho
//Association.addByAll(newField, AllFieldsByLowerName.get(keyName));
AllFieldsByLowerName.put(keyName, newField);
return newField;
}
Field addFieldAfter(Field afterField, String name) {
return addField(new Field(name.trim(), this), afterField, false);
}
Field addFieldBefore(Field beforeField, String name) {
return addField(new Field(name.trim(), this), beforeField, true);
}
protected Field addField(String name) {
return addField(new Field(name.trim(), this));
}
static LWComponent runtimeInitStyleNode(LWComponent style) {
if (style != null) {
// the INTERNAL flag permits the style to operate (deliver events) w/out a parent
style.setFlag(LWComponent.Flag.INTERNAL);
//style.clearFlag(LWComponent.Flag.INTERNAL); // no longer used to deliver events
// the DATA_STYLE bit is not persisted, must restore this bit manually:
style.setFlag(LWComponent.Flag.DATA_STYLE);
// no-longer principally need DATA_STYLE -- that's for the style object
// manually updating it's referrers if a DATA style property changes, such
// as the label, which is now handled via the selection, tho having this set
// also ensure label-template strings will never attempt to resolve (tho
// with no meta-data the style objects should never end up changing the
// template string even if they do attempt to resolve)
// as the style objects aren't proper children of the map, they never get this
// cleared, and we have to do it here manually to make sure
style.markAsRestored();
// Note that we don't even mark these with Flag.STYLE anymore, as their
// "styling" ability is now handled through the selection (and some
// old persisted styles may still have this bit set)
style.clearFlag(LWComponent.Flag.STYLE);
// hack to give the style a parent so it is considered "alive" and can deliver events
//style.setParent(FalseStyleParent);
}
return style;
}
/** interface {@link XMLUnmarshalListener} -- does nothing here */
public void XML_initialized(Object context) { mXMLRestoreUnderway = true; }
/** interface {@link XMLUnmarshalListener} -- does nothing here */
public void XML_fieldAdded(Object context, String name, Object child) {}
/** interface {@link XMLUnmarshalListener} -- does nothing here */
public void XML_addNotify(Object context, String name, Object parent) {}
// @Override
// public boolean equals(Object o) {
// if (this == o)
// return true;
// else if (o == null)
// return false;
// else {
// final Schema s;
// try {
// s = (Schema) o;
// } catch (ClassCastException e) {
// return false;
// }
// return mLocalID.equals(s.mLocalID);
// }
// }
// private Schema(Object source) {
// // would be very handy if source was a Resource and Resources had IO methods
// setSource(source);
// setGUID(edu.tufts.vue.util.GUID.generate());
// mLocalID = nextLocalID();
// }
/** for castor de-serialization only */
public Schema() {
mLocalID = nextLocalID();
Log.debug(this + "; CONSTRUCTED FOR CASTOR");
}
private static synchronized String nextLocalID() {
// must differentiate this from the the codes used for persisting nodes (just integer strings),
// as castor apparently uses a global mapping for all object types, instead of an id reference
// cache per-type (class).
return String.format("S%d", NextLocalId.incrementAndGet());
}
/** for persistance */
public final synchronized String getMapLocalID() {
return mLocalID;
}
/** @return true if this Schema contains data. Persisted Schema's are just references and do not contain data */
public synchronized boolean isLoaded() {
return mRows.size() > 0;
}
/** only debug during castor deserialization */
public final void setMapLocalID(String id) {
// we can safely ignore this -- a new current runtime map-local-id has already
// been set -- the existing one is purely for castor's use in assigning references
// from MetaMap's in the save file to this schema instance.
Log.debug(this + "; safely ignoring persisted map-id [" + id + "]");
//mLocalID.set(i);
}
public void setImageField(String name) {
if (name == null)
mImageField = null;
else
mImageField = findField(name);
}
public Field getImageField() {
return mImageField;
}
public synchronized void annotateFor(Collection<LWComponent> nodes) {
for (Field field : getFields())
field.annotateIncludedValues(nodes);
// is probably a faster way to track this by handling the key field specially
// during annotateIncludedValues above and process the rows v.s. the values,
// or just refactoring the whole process to go DataRow by DataRow.
final Field keyField = getKeyField();
final String keyFieldName = keyField.getName();
// // crap: this is where we should be comparing the actual data sourced schema.
// for (LWComponent node : nodes) {
// if (node.hasDataKey(keyFieldName) && !node.isSchematicField()) {
// final DataRow row = findRow(keyField, node.getDataValue(keyFieldName));
// if (row == null) {
// Log.warn("no raw data found for node " + node);
// continue;
// }
// final MetaMap rawData = row.getData();
// final MetaMap mapData = node.getRawData();
// }
// }
mContextRowNodeCount = 0;
for (DataRow row : mRows) {
final String rowKey = row.getValue(keyField);
row.mContextCount = 0;
row.setContextChanged(false);
for (LWComponent node : nodes) {
if (!node.isDataRow(this))
continue;
mContextRowNodeCount++; // todo: is wildly overcounting -- need to total at end by adding all final row.mContextCount's
if (!node.hasDataValue(keyFieldName, rowKey))
continue;
row.mContextCount++;
final MetaMap rawData = row.getData();
final MetaMap mapData = node.getRawData();
//Log.debug("comparing:\n" + rawData.values() + " to:\n" + mapData.values());
if (rawData != mapData) {
// todo: would be nice to tag each field to see what changed, tho
// that adds another bit for every single value in a data-set, and
// we have no per-value meta-data in the DataRow at the moment
row.setContextChanged(!rawData.equals(mapData));
}
// test if this node is a row node from this schema -- currently an imperfect test: only
// checks for presence of the same key field.
//if (node.hasDataKey(keyFieldName) && !node.isSchematicField()) {
// if (node.hasDataValue(keyFieldName) && !node.isSchematicField()) {
// final MetaMap rawData = row.getData();
// final MetaMap mapData = node.getRawData();
// Log.debug("comparing:\n" + rawData.values() + " to:\n" + mapData.values());
// row.setContextChanged(!rawData.equals(mapData));
// } else {
// // could set to a "present" status -- context changed state is now technically undefined...
// row.setContextChanged(false);
// }
}
}
}
public int getContextRowNodeCount() {
return mContextRowNodeCount;
}
public synchronized void flushData() {
if (DEBUG.Enabled) Log.debug("flushing " + this);
mRows.clear();
mLongestFieldName = 10; // for debug
for (Field f : getFields()) {
f.flushStats(); // flush data / enums, but keep any style
}
}
public synchronized DataRow findRow(Field field, String value) {
for (DataRow row : mRows)
if (row.contains(field, value))
return row;
return null;
}
@Override
public String toString() {
try {
String extra = "";
if (DEBUG.META) extra = " " + DSGUID + "/" + mResource;
return String.format("Schema@%08x[%s/%s; #%dx%d %s%s k=%s%s]",
System.identityHashCode(this),
""+mLocalID,
//""+GUID,
""+DSGUID,
mFields.size(),
mRows.size(),
Util.color(mName, Util.TERM_PURPLE),
isDiscarded ? Util.color(" DISCARDED", Util.TERM_RED) : "",
Relation.quoteKey(mKeyField),
extra);
} catch (Throwable t) {
t.printStackTrace();
return "Schema[" + t + "]";
}
}
public synchronized String getGUID() {
return GUID;
}
public synchronized void setGUID(String s) {
GUID = s;
}
synchronized void setDSGUID(String s) {
//if (DEBUG.Enabled) Util.printStackTrace("setDSGUID [" + s + "]");
if (DEBUG.SCHEMA) Log.debug(this + "; setDSGUID: " + s);
DSGUID = s;
getResource().setProperty("@DSGUID", s);
// if (DSGUID != null)
// registerByDSGUID(this, DSGUID);
}
synchronized String getDSGUID() {
return DSGUID;
}
/** set the node style object used for record/row nodes */
public synchronized void setRowNodeStyle(LWComponent style) {
if (DEBUG.SCHEMA) Log.debug("setRowNodeStyle: " + style);
runtimeInitStyleNode(style);
mStyleNode = style;
}
/** @return the node style used for record/row nodes */
public synchronized LWComponent getRowNodeStyle() {
return mStyleNode;
}
public synchronized Field getKeyField() {
if (mKeyField == null)
mKeyField = getKeyFieldGuess();
return mKeyField;
}
public synchronized Field getEncodingField() {
if (mEncodingField == null)
return new Field("win", this);
else
return mEncodingField;
}
public synchronized String getKeyFieldName() {
return getKeyField().getName();
}
public synchronized void setKeyField(Field f) {
//Log.debug("setKeyField " + Util.tags(f));
mKeyField = f;
}
public synchronized void setEncodingField(Field f) {
//Log.debug("setKeyField " + Util.tags(f));
mEncodingField = f;
}
public synchronized void setKeyField(String name) {
setKeyField(getField(name));
}
public synchronized void setEncodingField(String name) {
setEncodingField(getField(name));
}
// public Object getSource() {
// return mSource;
// }
// public void setSource(Object src) {
// mSource = src;
// try {
// setResource(src);
// } catch (Throwable t) {
// Log.warn(t);
// }
// }
// private void setResource(Object r) {
// if (r instanceof Resource)
// mResource = (Resource) r;
// else if (r instanceof InputSource)
// mResource = Resource.instance(((InputSource)r).getSystemId());
// else if (r instanceof File)
// mResource = Resource.instance((File)r);
// else if (r instanceof URL)
// mResource = Resource.instance((URL)r);
// else if (r instanceof String)
// mResource = Resource.instance((String)r);
// }
public synchronized void setResource(Resource r) {
Log.debug(this + "; setResource " + r);
mResource = r;
}
public synchronized Resource getResource() {
return mResource;
}
public synchronized int getRowCount() {
return mRows.size();
}
public synchronized void setName(String name) {
mName = name;
}
public synchronized Field getField(String name) {
if (name == null) {
Log.warn("searching for null Field name in " + this, new Throwable("HERE"));
return null;
}
final Field f = findField(name);
if (f == null && DEBUG.Enabled) Log.debug(String.format("%s; no field named '%s' in %s",
this,
name,
//mFields.keySet()
Util.tags(mFields.keySet())
));
return f;
}
/** will quietly return null if not found */
public synchronized Field findField(String name) {
Field f = mFields.get(name);
if (f == null && name != null && name.length() > 0 && Character.isUpperCase(name.charAt(0))) {
f = mFields.get(name.toLowerCase());
if (DEBUG.SCHEMA && f != null) Log.debug("found lowerized [" + name + "]");
}
return f;
}
// todo: doesn't use same case indepence as findField
public synchronized boolean hasField(String name) {
return mFields.containsKey(name);
}
// todo: doesn't use same case indepence as findField
public synchronized boolean hasField(Field f) {
// todo: more performant (keep a set of just hashed Fields)
// could just check f.getSchema() == this, tho that may not work at init time?
return mFields.containsKey(f.getName());
}
public static final String MATRIX_NAME_FIELD="Title";
public synchronized String getName() {
if (mName != null) {
return mName;
} else {
// for giving us something informative during construction
final Resource r = getResource();
String s = r.getTitle();
if (s == null)
return r.getSpec();
else
return s;
}
//return getSource().toString();
// String s = getSource().toString();
// int i = s.lastIndexOf('/');
// if (i < s.length() - 2)
// return s.substring(i+1);
// else
// return s;
// Object o = getSource();
// if (o instanceof File)
// return ((File)o).getName();
// else if (o instanceof URL)
// return ((URL)o).getFile();
// else
// return o.toString();
}
// private String[] matrixEntities = {"FromName","ToName","FlowAmount"};
private HashMap<String,Integer> matrixColNums = new HashMap<String,Integer>();
private HashMap<String,Integer> matrixMetadataCols = new HashMap<String, Integer>();
private Field matrixNameField = new Field(MATRIX_NAME_FIELD,this);
public synchronized void ensureFields(String[] names) {
ensureFields(null,names,false);
}
public void ensureFields(XmlDataSource ds, String[] names, boolean isMatrixData) {
if (DEBUG.SCHEMA) {
Log.debug("ensureFields:");
Util.dump(names);
Log.debug("ensureFields; existingFields:");
Util.dump(mFields);
}
if (isMatrixData)
addField(matrixNameField);
int index = 0;
for (String name : names) {
name = name.trim();
if (isMatrixData)
{
String[] matrixEntities = {ds.getMatrixRowField(),ds.getMatrixColField(),ds.getMatrixRelField(),ds.getMatrixPivotField()};
if (isMatrixData && org.apache.commons.lang.ArrayUtils.contains(matrixEntities, name))
{ matrixColNums.put(name, index);
index++;
continue;
} else if (isMatrixData && !org.apache.commons.lang.ArrayUtils.contains(matrixEntities, name))
matrixMetadataCols.put(name,index);
}
if (!mFields.containsKey(name)) {
addField(new Field(name, this));
}
index++;
}
// TODO: if any fields already exists and are NOT named in names, we at least
// need debug here: the schema has changed! (which may be normal for XML --
// e.g., a news feed) What to do? If we don't clear them out they'll remain
// as empty fields in the data-tree.
}
public synchronized void ensureFields(int count) {
int curFields = mFields.size();
if (count <= curFields)
return;
for (int i = curFields; i < count; i++) {
String name = "Column " + (i+1);
addField(new Field(name, this));
}
}
// public void createFields(String[] names) {
// for (String name : names)
// mFields.put(name, new Field(name, this));
// }
// public void createFields(int count) {
// for (int i = 0; i < count; i++) {
// String name = "Column " + (i+1);
// mFields.put(name, new Field(name, this));
// }
// }
private static boolean isUnlikelyKeyField(Field f) {
// hack for dublin-core fields (e.g., dc:creator), which may often
// all be unique (e.g., short RSS feed), but are unlikely to be useful keys.
return f.isSingleValue() || (f.getName().startsWith("dc:") && !f.getName().equals("dc:identifier"));
}
// this returns a Vector so can be fed directly to a JComboBox if desired
public synchronized Vector<String> getPossibleKeyFieldNames() {
final Vector<String> possibleKeyFields = new Vector();
for (Field field : getFields())
if (field.isPossibleKeyField())
possibleKeyFields.add(field.getName());
if (possibleKeyFields.size() == 0) {
// add them all: some data rows are probably duplicates, and we can't
// identify a key field
for (Field field : getFields())
if (!field.isSingleton())
possibleKeyFields.add(field.getName());
}
return possibleKeyFields;
}
// this returns a Vector so can be fed directly to a JComboBox if desired
public synchronized Vector<String> getFieldNames() {
final Vector<String> names = new Vector();
for (Field field : getFields())
names.add(field.getName());
return names;
}
/** look at all the Fields and make a guess as to which is the most likely key field
* This currently will always return *some* field, even if it's not a possible key field. */
public synchronized Field getKeyFieldGuess() {
if (getFieldCount() == 0)
throw new Error("no fields in " + this);
// Many RSS feeds can be covered by checking "guid" and "link"
Field f;
if ((f = findField("guid")) != null && f.isPossibleKeyField())
return f;
if ((f = findField("key")) != null && f.isPossibleKeyField())
return f;
//if ((f = getField("link")) != null && f.isPossibleKeyField()) // some rss news feeds have dupe entries
if ((f = findField("link")) != null && !f.isSingleValue())
return f;
// todo: identifying the shortest field isn't such a good strategy
// todo: prioritize Fields who's values are all integers
Field firstField = null;
Field shortestField = null;
int shortestFieldLen = Integer.MAX_VALUE;
for (Field field : getFields()) {
if (firstField == null)
firstField = field;
if (field.isPossibleKeyField() && !isUnlikelyKeyField(field)) {
if (field.getMaxValueLength() < shortestFieldLen) {
shortestField = field;
shortestFieldLen = field.getMaxValueLength();
}
}
}
// if (shortestField == null) {
// for (Field field : getFields()) {
// if (field.getMaxValueLength() < shortestFieldLen) {
// shortestField = field;
// shortestFieldLen = field.getMaxValueLength();
// }
// }
// }
final Field guess = shortestField == null ? firstField : shortestField;
if (guess == null) {
//Log.warn("no key field: " + this, new Throwable("FYI"));
// Log.warn("key field guess failed in: " + this);
// dumpSchema(System.err);
throw new Error("no key field found in " + this);
}
return guess;
}
public void dumpSchema(PrintStream ps) {
dumpSchema(new PrintWriter(new OutputStreamWriter(ps)));
}
public void dumpSchema(PrintWriter ps) {
ps.println(getClass().getName() + ": " + toString());
final int pad = -mLongestFieldName;
final String format = "%" + pad + "s: %s\n";
for (Field f : getFields()) {
ps.printf(format, f.getName(), f.valuesDebug());
}
//ps.println("Rows collected: " + rows.size());
}
public String getDump() {
StringWriter debug = new StringWriter();
dumpSchema(new PrintWriter(debug));
return debug.toString();
}
protected void addRow(DataRow row) {
mRows.add(row);
}
protected void addRow(String[] values) {
DataRow row = new DataRow(this);
int i = 0;
for (Field field : getFields()) {
final String value;
try {
value = values[i++];
} catch (IndexOutOfBoundsException e) {
Log.warn("missing value at index " + (i-1) + " for field " + field + " in " + Arrays.asList(values));
Util.dumpArray(values);
row.addValue(field, "<missing>");
continue;
}
row.addValue(field, value);
}
addRow(row);
}
protected HashMap<String,Integer> existingRows = null;
List<MatrixRelationship> matrixRelations = new ArrayList<MatrixRelationship>();
public boolean isMatrixDataSet = false;
protected void addMatrixRow(XmlDataSource ds, String[] values) {
isMatrixDataSet=true;
DataRow fromRow = new DataRow(this);
Collection<Integer> colNums = matrixColNums.values();
//HANDLE FROM SIDE OF RELATIONSHIP
String rowName = ds.getMatrixRowField();
String colName = ds.getMatrixColField();
String relName = ds.getMatrixRelField();
fromRow.addValue(matrixNameField, values[matrixColNums.get(rowName)]);
for (Field field : getFields()) {
if (field.getName().equals(MATRIX_NAME_FIELD))
continue;
int valCol = this.matrixMetadataCols.get(field.getName()).intValue();
fromRow.addValue(field, values[valCol]);
}
if (existingRows.get(values[matrixColNums.get(rowName)]) == null)
{
addRow(fromRow);
existingRows.put(values[matrixColNums.get(rowName)], new Integer(fromRow.mmap.size()));
} else
{
//it's currently in as a ToRow replace it
int valCount = existingRows.get(values[matrixColNums.get(rowName)]);
if (valCount < fromRow.mmap.size())
{
//remove the existing row, add new one.
existingRows.remove(values[matrixColNums.get(rowName)]);
existingRows.put(values[matrixColNums.get(rowName)], new Integer(fromRow.mmap.size()));
for (DataRow r: getRows())
{
if (r.getValue(MATRIX_NAME_FIELD).equals(values[matrixColNums.get(rowName)]))
{
getRows().remove(r);
break;
}
}
addRow(fromRow);
}
}
//HANDLE TO SIDE OF RELATIONSHIP
//don't add self-referntial relationships.
if (!values[matrixColNums.get(rowName)].equals(values[matrixColNums.get(colName)]))
{
DataRow toRow = new DataRow(this);
toRow.addValue(matrixNameField, values[matrixColNums.get(colName)]);
//toRow won't have any fields :(
if (existingRows.get(values[matrixColNums.get(colName)]) == null)
{
addRow(toRow);
existingRows.put(values[matrixColNums.get(colName)], new Integer(toRow.mmap.size()));
}
if (!values[matrixColNums.get(rowName)].equals(values[matrixColNums.get(colName)]) && !(values[matrixColNums.get(relName)].equals("0")))
matrixRelations.add(new MatrixRelationship(values[matrixColNums.get(rowName)],values[matrixColNums.get(colName)],values[matrixColNums.get(relName)]));
}
return;
}
int i=0;
String scriptTemplate;
protected void addWideMatrixRow(XmlDataSource ds, String[] values) {
String script = scriptTemplate;
final Interpreter interpreter = new Interpreter();
int matrixSize = new Integer(ds.getMatrixSizeField()).intValue();
int rowCount = tempTable.values().size();
// System.out.println("Matrix Size :" + matrixSize);
//skip partial rows
if (values.length < matrixSize)
return;
isMatrixDataSet=true;
int pivotNum = 0;
String thisCol = "";
DataRow fromRow = null;
if (rowCount < matrixSize)
{
fromRow = new DataRow(this);
String matrixPivot = ds.getMatrixPivotField();
pivotNum = matrixColNums.get(matrixPivot);
thisCol = values[matrixColNums.get(matrixPivot)];
fromRow.addValue(matrixNameField, thisCol);
for (int i= pivotNum + 1; i < pivotNum + matrixSize + 1; i++)
{
if (thisCol.equals(ds.headerValues[i]))
continue;
//System.out.println("RELATION " + (values[i].equals("0")));
// System.out.println("ds.getMatrixIgnore " + ds.getMatrixIgnoreField());
if (!(values[i].equals(ds.getMatrixIgnoreField())))
{
Object res="";
if (scriptTemplate !=null)
{
try {
// System.out.println("before : " + scriptTemplate);
script = scriptTemplate.replaceAll("(\\$rel)", values[i]);
// System.out.println(script);
interpreter.eval(script);
res = interpreter.get("result");
// System.out.println(res.toString());
} catch (EvalError e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
else
res = values[i];
matrixRelations.add(new MatrixRelationship(thisCol,ds.headerValues[i],res.toString()));
}
}
//add everything before the matrix as metadata
for (int i=0; i < pivotNum+1; i++)
{
fromRow.addValue(new Field(ds.headerValues[i],this), values[i]);
}
//add everything after the matrix as metadata
for (int i = pivotNum+ matrixSize +1; i < values.length; i ++)
{
fromRow.addValue(new Field(ds.headerValues[i],this), values[i]);
}
//addRow(fromRow);
// System.out.println("ADD TO HASH : " + thisCol + " ::: " + matrixColNums.get(matrixPivot));
addRowToHash(thisCol,fromRow);
}
else
{
String matrixPivot = ds.getMatrixPivotField();
pivotNum = matrixColNums.get(matrixPivot);
//add everything after the matrix as metadata
for (int i = pivotNum+1; i < matrixSize; i++)
{
//System.out.println("val : " + values[i] + " ::: "+ ds.headerValues[i]);
DataRow r = tempTable.remove(ds.headerValues[i]);
r.addValue(new Field(values[pivotNum],this),values[i]);
tempTable.put(ds.headerValues[i], r);
// fromRow.addValue(new Field(ds.headerValues[i],this), values[i]);
}
// System.out.println("END OF ROW");
}
//we're passed the point of using this for the matrix so use it as metadata.
return;
}
protected void convertToRows()
{
int i=0;
for (DataRow r : tempTable.values())
{
//System.out.println(i++);
if (r !=null)
addRow(r);
}
tempTable.clear();
// tempTable=null;
}
TreeMap<String,DataRow> tempTable = new TreeMap<String,DataRow>();
private DataRow getRowFromHash(String key)
{
return tempTable.get(key);
}
private void addRowToHash(String key, DataRow val)
{
tempTable.put(key, val);
}
/** called by schema loaders to notify the Schema that all the rows have been loaded (and any final analysis can be completed) */
synchronized void notifyAllRowsAdded() {
for (Field f : Util.toArray(getFields(), Field.class)) { // must dupe as will may be adding new fields (quartiles)
if (DEBUG.Enabled) Log.debug("field analysis " + Relation.quoteKey(f.getName()) + ": type " + Util.tags(f.getType()));
f.setStyleNode(DataAction.makeStyleNode(f));
try {
f.performFinalAnalysis();
} catch (Throwable t) {
Log.error("analysis failed on " + f, t);
}
}
}
public synchronized List<DataRow> getRows() {
return mRows;
}
// /** @return rows that match the given key/value pair, allowing for user specified key associations */
// public Collection<DataRow> getMatchingRows(Field field, String fieldValue)
// {
// if (!hasField(field)) {
// Log.debug("getMatchingRows: immediate return, field not in Schema: " + field, new Throwable("HERE"));
// return Collections.EMPTY_LIST;
// }
// final Collection<DataRow> results = new HashSet();
// // todo: more performant than HashSet? perhaps just scan at end for dupes
// Relation.searchDataWithField(field, fieldValue, getRows(), results);
// return results;
// }
/** @return rows that match the given key/value pair, allowing for user specified key associations */
public Collection<DataRow> getMatchingRows(Field field, String fieldValue)
{
if (DEBUG.Enabled) Log.debug("getMatchingRows: " + Relation.quoteKV(field, fieldValue));
final Collection<DataRow> results = new HashSet();
// todo: more performant than HashSet? perhaps just scan at end for dupes
if (hasField(field)) {
Relation.searchDataWithField(field, fieldValue, getRows(), results);
} else {
// todo performance: getCrossSchemaRelation is recomputing all sorts of
// stuff each time that we could do faster if we unrolled in one place --
// e.g., make it take a collection of row-data (all from the same schema, so
// maybe actually hand it a schema), and it could handle it there. Plus,
// even if NO join's were found, it will still check all rows!
//if (Relation.getCrossSchemaRelation(field, row.getData(), fieldValue) != null)
// NOTE: WE RISK RECURSION HERE! getCrossSchemaRelation calls getMatchingRows...
// will that somehow auto-jump any length of schema relation joins?
//-----------------------------------------------------------------------------
// SHIT! Do we actually need the full getCrossSchemaRelation checks (for Mediums case tho, right??
// But don't we want to check straight Associations first? This is overkill for those cases.
//
// THE CASE WE'RE MISSING IS THIS: (to handle IN getCrossSchemaRelation,
// which should become a more generic getRelations) -- if the SEARCH field
// (e.g., medium) is DIFFERENT than the field found in the Association,
// only THEN do we need to attempt a join, otherwise, we can use a straight
// Association.
//
//-----------------------------------------------------------------------------
// if (Association.hasAliases(this, field)) {
// }
if (Association.hasJoins(this, field)) {
for (DataRow row : getRows()) {
if (Relation.getCrossSchemaRelation(field, row.getData(), fieldValue) != null)
results.add(row);
}
}
}
return results;
}
/** @param data: a MetaMap from another schema. Heuristics will be used,
* including looking at associations, to determine what should match.
*/
public Collection<DataRow> getMatchingRows(MetaMap searchKeys)
{
if (DEBUG.Enabled) Log.debug("getMatchingRows: " + Util.tags(searchKeys));
final Collection<DataRow> matching = new HashSet();
// we use a HashSet to prevent duplicates, which could happen through
// duplicate associations, or associations that are duped by an auto-join
Relation.searchDataWithRow(searchKeys, getRows(), this, matching);
return matching;
}
// public Collection<DataRow> getJoinedMatchingRows(Association join, Schema joiningSchema)
// {
// // should we even try this? something like the below: cut & pasted from DataAction
// final Field indexKey = join.getFieldForSchema(dragSchema);
// final String indexValue = onMapRowData.getString(join.getKeyForSchema(dropSchema)); // todo: multi-values
// Log.debug("JOIN: indexKey=" + indexKey + "; indexValue=" + indexValue);
// final Collection<DataRow> matchingRows = getMatchingRows(indexKey, indexValue);
// Log.debug("found rows: " + Util.tags(matchingRows));
// return matchingRows;
// }
public synchronized Collection<Field> getFields() {
//return mFields.values();
return mFieldList;
}
public synchronized Collection<Field> getXMLFields() {
if (mXMLRestoreUnderway) {
// return the list to be *loaded* by castor
return mPersistFields;
} else
return getFields();
}
public synchronized int getFieldCount() {
return getFields().size();
//return mFields.size();
}
/** if a singleton exists with the given name, return it's value, otherwise null */
public synchronized String getSingletonValue(String name) {
Field f = mFields.get(name);
if (f != null && f.isSingleton())
return f.getValueSet().iterator().next();
else
return null;
}
public synchronized boolean isXMLKeyFold() {
return mKeyFold;
}
public synchronized void setXMLKeyFold(boolean keyFold) {
mKeyFold = keyFold;
}
}
/** a row impl that handles flat tables as well as Xml style variable "rows" or item groups */
//class DataRow extends tufts.vue.MetaMap {
final class DataRow implements Relation.Scannable{
final tufts.vue.MetaMap mmap = new tufts.vue.MetaMap();
boolean isContextChanged;
int mContextCount;
DataRow(Schema s) {
mmap.setSchema(s);
}
void setContextChanged(boolean t) {
isContextChanged = t;
}
boolean isContextChanged() {
return isContextChanged;
}
public int getContextCount() {
return mContextCount;
}
/** add the given value, and track for analysis */
void addValue(Field f, String value) {
f.trackValue(takeValue(f, value));
}
/** add, but DO NOT track the value -- return the actual value added (which may have been trimmed or be Field.EMPTY_VALUE) */
String takeValue(Field f, String value) {
value = value.trim();
// final String existing = values.put(f, value);
// if (existing != null && Schema.DEBUG)
// Log.debug("ROW SUB-KEY COLLISION " + f);
//super.put(f.getName(), value);
if (value.length() == 0)
value = Field.EMPTY_VALUE;
mmap.put(f.getName(), value);
return value;
}
Iterable<Map.Entry> dataEntries() {
return mmap.entries();
}
public String getValue(String key) {
return mmap.getString(key);
}
/** interface Scannable */
public Collection<String> getValues(String key) {
return mmap.getValues(key);
}
public Collection<String> getValues(Field f) {
return mmap.getValues(f.getName());
}
/** interface Scannable */
public Schema getSchema() {
return mmap.getSchema();
}
/** interface Scannable */
public String getString(String key) {
return mmap.getString(key);
}
/** interface Scannable */
public boolean hasEntry(String key, CharSequence value) {
return mmap.hasEntry(key, value);
}
String getValue(Field f) {
return mmap.getString(f.getName());
//return super.getString(f.getName());
}
boolean contains(Field field, Object value) {
return value != null && value.equals(getValue(field));
}
@Override public String toString() {
return mmap.values().toString();
}
tufts.vue.MetaMap getData() {
return mmap;
}
}