package edu.tufts.sds.metadata;

import edu.tufts.sds.ontology.*;
import static edu.tufts.sds.metadata.VueMetadataElement.*;
import tufts.Util;
import tufts.sds.DEBUG;
import tufts.sds.MOXYmetadataElement;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class MetadataListManager implements tufts.sds.XMLUnmarshalListener
{
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(MetadataListManager.class);

private final static boolean DEBUG_LOCAL = false;

static { Log.info(“ONTOLOGY_NONE ” + Util.tags(ONTOLOGY_NONE) + ” ” + Util.tag(ONTOLOGY_NONE)); }

// CSH DataList holds the categories & keywords assigned the node under inspection.
private final CategoryFirstList dataList = new CategoryFirstList();

private final SubsetList categoryList = new SubsetList(VueMetadataElement.CATEGORY);
private final SubsetList ontologyList = new SubsetList(VueMetadataElement.ONTO_TYPE);
private final SubsetList otherList = new SubsetList(VueMetadataElement.OTHER);

public MetadataListManager() {
// todo: this sub-list thing by type is needlessly complicated, and
// not needed at all.

// Can only find one code reference to RESOURCE_CATEGORY — in MapInspectorPanel where
// VUE sets dcCreator meta-data if the LWMap author is edited.
// In ~/Maps I found 2 instances of type RESOURCE_CATEGORY — one map with dc:creator, and
// another with dc:description, which is what saved LWMap notes are using.

// Both #none and #TAG ARE handled in the ui, but ONLY when they’re type is 1 (CATEGORY) or
// 2 (ONTO_TYPE) — the UI only pulls by TYPE, not key. Of course, the combo-box no longer
// supports TAG, and the tag in this instance is “#Tag” not “#TAG” — so the UI pulls up
// whatever is after the # if it’s of a VME this.type it handles.
}

/** horrible API still called all over the place */
public List<VueMetadataElement> getMetadata() { return dataList; }
public List<VueMetadataElement> getAll() { return dataList; }

/** is this *effectively* empty */
public boolean isEmpty() {
return dataList.size() <= 0 || (dataList.size() == 1 && dataList.get(0).isEmpty());
}

// SMF 2013-05-23: Testing showed that sizeForTypeHistorical is reporting wrong, which means it
// was never right! Thus a node may still be overreporting the presence of meta-data — need
// to check for that.

/** is there any data of the given type in here? this is a special call for the LWIcon UI */
public boolean hasMetadata(int type) {
return sizeForTypeHistorical(type) > 0;
// if (true) return getOldMetadataHTMLAsPresenceTest(type).length() > 0;
// final int count = countType(type);
// final int size = sizeForType(type);
// if (count != size) Log.warn(“count != size for type ” + type + “: ” +count + ” != ” + size); // they don’t match!!!
// return size > 0 && !isEmpty();
}

private int countType(int type) {
int count = 0;
for (VueMetadataElement vme : dataList)
if (vme.getType() == type)
count++;
return count;
}

/** note: as is from old impl: only checks for CAT/REST/ONTO, defaults to OTHER */
private int sizeForType(int type) {
return sizeForTypeHistorical(type);

}

private int sizeForTypeHistorical(int type) {
switch (type) {
case CATEGORY: return getCategoryListSize();
case ONTO_TYPE: return getOntologyListSize();
case RESOURCE_CATEGORY: return getResourceListSize();
case OTHER:
case SEARCH_STATEMENT:
case TAG:
default:
return getOtherListSize();
}
}

private int sz(int type) { return sizeForTypeActual(type); }

private int sizeForTypeActual(int type) {
switch (type) {
case TAG: return -1;
case CATEGORY: return getCategoryListSize();
case ONTO_TYPE: return getOntologyListSize();
case SEARCH_STATEMENT: return -1;
case OTHER: return getOtherListSize();
case RESOURCE_CATEGORY: return getResourceListSize();
}
return Integer.MIN_VALUE;
}

private boolean unmarshalling = false;
/** @see tufts.sds.XMLUnmarshalListener */
public void XML_initialized(Object context) { unmarshalling = true; }
public void XML_completed(Object context) { unmarshalling = false; }
public void XML_fieldAdded(Object context, String name, Object child) {}
public void XML_addNotify(Object context, String name, Object parent) {}

public List<MOXYmetadataElement> getMOXYdata() {
// TODO: FIX
// if (isEmpty())
// return null;
// else
List<MOXYmetadataElement> mlist = new ArrayList<MOXYmetadataElement>();
for (VueMetadataElement e : dataList)
mlist.add(new MOXYmetadataElement(e.getType(),e.getKey(),e.getValue(),null));
return mlist;
}

/** for castor persistence only */
public List<VueMetadataElement> getXMLdata() {
// TODO: FIX
// if (isEmpty())
// return null;
// else
return dataList;
}

/** for castor persistence only */
public List<VueMetadataElement> getSetterXMLdata() {
if (unmarshalling) {
// allow old “metadata” named element name to initialize the list
return dataList;
} else {
// prevent persistence of old “metadata” named element name
return null;
}
}

// public boolean intersects(MetadataList remote) {
// final List remoteList = remote.getMetadata();
// for (VueMetadataElement e : dataList)
// if (remoteList.contains(e))
// return true;
// return false;
// }

/** attempt at high-performace bulk load that triggers no GUI updates — WARNING: ONLY ADDS TO “CATEGORY” LIST */
public void add(Iterable<Map.Entry> kvEntries) {
// oddly, firing list changed events get much slower as amount of total [map] meta-data increases [NOW WE KNOW WHY]

// the categoryList updates still trigger the updates, so we turn them off with a flag for now

// disableEvents = true;
try {
for (Map.Entry e : kvEntries) {
try {
categoryList.add(new VueMetadataElement(e.getKey().toString(), e.getValue().toString()));
} catch (Throwable t) {
Log.error(“add entry ” + Util.tags(e), t);
}
}
} catch (Throwable tx) {
Log.error(“add iterable ” + Util.tags(kvEntries), tx);
}
// finally {
// disableEvents = false;
// }
fireListChanged();
}

public void addElementByKeyValue(String key, String value) {
addElement(new VueMetadataElement(key, value));
}
public void addElement(VueMetadataElement element) {
dataList.add(element);
fireListChanged();
}

// public VueMetadataElement get(int i) {
// return dataList.get(i);
// }
//
// public void removeCategoryType(String key, String value)
// {
// int foundIndex = -1;
//
// for (int i = 0; i < getCategoryListSize(); i++) {
// final VueMetadataElement vme = getCategoryListElement(i);
// if (vme.getKey().equals(key) && vme.getValue().equals(value)) {
// foundIndex = i;
// break;
// }
// }
// if (foundIndex != -1)
// remove(foundIndex);
// }

public void remove(int i) {
dataList.remove(i);
fireListChanged();
}

// public boolean removeAnyType(VueMetadataElement target) {
// return dataList.remove(target);
// }

public int size() {
return dataList.size();
}

private int getResourceListSize() {
// what??? it’s called “CategoryFirstList”, yet it stores resources 1st?
return dataList.getResourceEndIndex();
}
public int getCategoryListSize() { // publicly called InspectorPane
return
dataList.getCategoryEndIndex() –
dataList.getResourceEndIndex();
}
public int getOntologyListSize() { // publicly called
return
dataList.getOntologyEndIndex() –
dataList.getCategoryEndIndex();
}
private int getOtherListSize() {
return
dataList.getOtherEndIndex() –
dataList.getOntologyEndIndex();
}

/** This replaces the first item with the same matching key, for CATEGORY and RESOURCE_CATEGORY types only.
* This replaces the value in the existing VME, to allow for multple replacements at once */
public void replaceValueForKey(VueMetadataElement hasNewValue) {
final int index;
if (hasNewValue.type == VueMetadataElement.CATEGORY)
index = findCategory(hasNewValue.key);
else if (hasNewValue.type == VueMetadataElement.RESOURCE_CATEGORY)
index = findRCategory(hasNewValue.key);
else
throw new UnsupportedOperationException(“type of ” + hasNewValue);

if (index >= 0) {
//dataList.set(index, hasNewValue);
final VueMetadataElement hasOldValue = dataList.get(index);
hasOldValue.setValue(hasNewValue.value);
if (hasOldValue.obj instanceof String[]) // should always be the case
((String[])hasOldValue.obj)[1] = hasNewValue.value;
fireListChanged();
}
}

/*public int categoryIndexOfFirstWithValueAndKey(String key,String value) { }*/

public VueMetadataElement get(String key) {
int rcindex = findRCategory(key);
int cindex = findCategory(key);
if(rcindex != -1)
return getResourceListElement(rcindex);
if(cindex != -1)
return getCategoryListElement(cindex);
else
return null;
}
/** finds the first entered (last in order) category element with the supplied key
* @return -1 if not found. **/
private int findCategory(String key) {
VueMetadataElement vme = null;
int i = -1;
try {
for (i=0;i<getCategoryListSize();i++) {
vme = getCategoryListElement(i);
if (key.equals(vme.key))
return i;
}
} catch (Throwable t) {
Log.error(“searching for ” + Util.tags(key) + ” at index ” + i + ” with ” + vme, t);
}
return -1;
}
// See MapInspectorPanel
/*public*/ public int findRCategory(String key)
{
for(int i=0;i<getResourceListSize();i++) {
VueMetadataElement vme = getResourceListElement(i);
if(vme.getKey().equals(key))
return i;
}
return -1;
}

public boolean contains(VueMetadataElement vme) {
return contains(vme.getKey(), vme.getValue());
}

public boolean contains(String key, String value)
{
for(int i=0;i<getCategoryListSize();i++) {
VueMetadataElement vme = getCategoryListElement(i);
if(vme.getKey().equals(key) && vme.getValue().equals(value))
return true;
}
return false;
}

private boolean containsOntologicalType(String ontType)
{
for(int i=0;i<getOntologyListSize();i++) {
VueMetadataElement vme = getOntologyListElement(i);
vme.getObject();
if(DEBUG_LOCAL) Log.debug(“containsOntologicalType – vme.getValue() ” + vme.getValue() + ” — ontType from properties ” + ontType);
if(ontType.equals(vme.getValue())) {
return true;
}
}
return false;
}

/*public*/ public VueMetadataElement getCategoryListElement(int index)
{
try {
if(getCategoryListSize() > 0 && index < dataList.size())
return dataList.get(index+((CategoryFirstList)dataList).getResourceEndIndex());
//else
//return new VueMetadataElement(); // SMF?
}
catch(Exception e) {
Log.warn(“getCategoryListElement ” + index, e);
return null;
//return new VueMetadataElement();
}
return null;
}
private VueMetadataElement getResourceListElement(int index) {
try {
if (getResourceListSize() > 0 && index < dataList.size())
return dataList.get(index);
else
return new VueMetadataElement();
}
catch(Exception e) {
Log.warn(“getResourceListElement ” + index, e);
return new VueMetadataElement();
}
}
private void setCategoryListElement(int i, VueMetadataElement vme) {
final int index = i + dataList.getResourceEndIndex();
try {
//if (getCategoryListSize() > 0 && index < dataList.size())
if (dataList.size()==0 ) {

dataList.add(vme);
}
else if (i>=dataList.size())
dataList.add(vme);
else
dataList.set(index, vme);
} catch(Exception e) {
Log.warn(“setCategoryListElement ” + i + “, real=” + index + “; ” + vme, e);
return;
}
}
private void setResourceListElement(int index, VueMetadataElement ele)
{
try {
if(getResourceListSize() > 0 && index < dataList.size())
dataList.set(index,ele);
else
return;
}
catch(Exception e) {
Log.warn(“setResourceListElement ” + index + “; ” + ele, e);
return;
}
}

public VueMetadataElement getOntologyListElement(int i)
{
CategoryFirstList cf = ((CategoryFirstList)dataList);
int index = i+cf.getCategoryEndIndex();
try
{
if(getOntologyListSize() > 0 && index < dataList.size())
return dataList.get(index);
else
return new VueMetadataElement();
}
catch(Exception e)
{
return new VueMetadataElement();
}
}

// See Util.getMergeProperty — note: was always persisted. Now is never persisted.
/*public*/ public String getOntologyListString()
{
String returnString = “”;

for(int i=0;i<getOntologyListSize();i++)
{
VueMetadataElement vme = getOntologyListElement(i);
returnString += vme.getObject() + “|”;
}

return returnString;
}
private void setOntologyListElement(int i,VueMetadataElement ele)
{
CategoryFirstList cf = ((CategoryFirstList)dataList);
int index = i+cf.getCategoryEndIndex();
try
{
if(getOntologyListSize() > 0 && index < dataList.size())
dataList.set(index,ele);
else
return;
}
catch(Exception e)
{
return;
}
}

private VueMetadataElement getOtherListElement(int i)
{
int index = i+((CategoryFirstList)dataList).getOntologyEndIndex();
try
{
if(getOtherListSize() > 0 && index < dataList.size())
return dataList.get(index);
else
return new VueMetadataElement(); // NOTE VME CREATION!
}
catch(Exception e)
{
return new VueMetadataElement(); // NOTE VME CREATION!
}
}

private void setOtherListElement(int i,VueMetadataElement ele)
{
int index = i+((CategoryFirstList)dataList).getOntologyEndIndex();
try
{
if(getOtherListSize() > 0 && index < dataList.size())
dataList.set(index,ele);
else
return;
}
catch(Exception e)
{
return;
}
}

public String getMetadataAsHTML(int type) {
if (DEBUG.DATA || DEBUG.DR) // DR so can turn on with ‘^’ key
return buildDebugHTML(type).toString();
else
return buildHTMLForType(type);
}

private StringBuilder buildDebugHTML(int type) {
final StringBuilder b = new StringBuilder();

// Old (still, actually) source-map meta-data that is put into nodes on merged-maps was
// supposed to be of type 4/OTHER, however apparently that never worked, and it was put in
// and saved out as type 0/TAG (E.g. ForcesAlicePulman.xxx). However, a bug or intentional
// very confusing hack had 0/TAG meta-data pulled for HTML even when meta-data of type
// 4/OTHER was requested. What a mess. Actually, the real problem may be that
// the TAG data actually exists in the “getOtherList()” decorator.

final String typeName;
if (type >= VueMetadataElement._Types.length)
typeName = “<unknown:” + type + “>”;
else
typeName = VueMetadataElement._Types[type];
//b.append(“<font face=Menlo size=-2><b>”); // fixed witdth font
b.append(“<font face=Menlo size=-0><b>”); // fixed witdth font
b.append(“Type requested: “).append(type).append(” / “).append(typeName);
b.append(“<br>”);
b.append(“real-sizes: “);
for (int i = 0; i <= 5; i++) {
final int sz = sizeForTypeActual(i);
if (sz >= 0)
b.append(VueMetadataElement._Types[i].toLowerCase()) .append(‘=’) .append(sz) .append(” “);
}
b.append(“<br>”);
b.append(“fake-sizes: “);
for (int i = 0; i <= 5; i++) {
final int sz = sizeForTypeHistorical(i);
if (sz >= 0)
b.append(VueMetadataElement._Types[i].toLowerCase()) .append(‘=’) .append(sz) .append(” “);
}
int count = 0;
for (VueMetadataElement md: dataList) {
count++;
b.append(“<br>#”);
if (count < 10) b.append(‘ ‘);
b.append(count).append(” “);
b.append(md.getType()).append(‘:’);
//b.append(String.format(“%14s”, md.getKey())); // not in HTML
final String key = md.getKey();
final String val = md.getValue();

if (dataList.size() > 1)
for (int i = key.length(); i < 22; i++) b.append(“&nbsp;”);

b.append(key).append(“: “);
if (val == null) {
b.append(“<font color=blue>”);
b.append(“null”);
final Object o = md.getObject();
if (o != null) {
boolean dumpObj = true;
if (o instanceof String[]) {
String[] s = (String[]) o;
if (s.length == 2 && s[0] == key && s[1] == val)
dumpObj = false;
}
if (dumpObj)
b.append(” “) .append(Util.tags(o)); // [nice to pass in a “StringPainter” to tags]
}
} else {
b.append(“<font color=red>”);
b.append(‘”‘);
if (val.length() > 64)
b.append(val.substring(0,64)) .append(“\”…x”) .append(val.length());
else
b.append(val) .append(‘”‘);
}
b.append(“</font>”);
}
return b;
}

private static final char SPACE = ‘ ‘, SLASH = ‘/’, NEWLINE = ‘\n’;

private String buildHTMLForType(final int typeRequest)
{
// Util.printStackTrace(“buildHTMLForType ” + typeRequest);

final StringBuilder b = new StringBuilder(32);
int startSize = 0;

if (typeRequest == OTHER) {
b.append(“&nbsp;<b><font color=gray>Merge sources:</font></b>”);
startSize = b.length();
buildHTMLForMergeSources(b);
} else {
buildHTMLFilteredAndOrganized(b, typeRequest);
// attempting to force a tiny last-line height to push rollover bottom edge down by a few pixels:
// b.append(“<font size=-14 color=gray>—“); // can’t get small enough: font size has floor on it
}
if (DEBUG.Enabled) {
// Log.debug(“got HTMLforType ” + typeRequest + ” [” + b + “]”);
if (b.length() == startSize) {
//Log.debug(“no html added, building debug html…”);
b.append(buildDebugHTML(typeRequest));
}
}

return b.toString();
}

/* don’t include empty values, and put keyless data at the end */
private void buildHTMLFilteredAndOrganized(final StringBuilder b, final int typeRequest)
{
final List<VueMetadataElement> haveKey = new ArrayList(dataList.size());
final List<VueMetadataElement> haveOnlyValue = new ArrayList(dataList.size());

for (VueMetadataElement md : dataList) {
// “<missing>” is a special value produced by the Schema code. It can’t be displayed
// for the same reason that <anythingInAngleBrackets> wont show up in the HTML, and for
// rollovers it’s also a good indicator of a non-interesting value.
if (md.type != typeRequest || !md.hasValue() || “<missing>”.equals(md.value))
continue;
if (md.key == null || md.key == ONTOLOGY_NONE || md.key == KEY_TAG) {
haveOnlyValue.add(md);
} else {
haveKey.add(md);
}
}
if (haveKey.size() == 1 && haveOnlyValue.size() == 0) {
// If single key & value, display it specially. We may want to reserve this for
// the @ValueOf cases (enumerated values from a Schema), but we just do it for
// everything now.
b.append(” &nbsp; <b><font color=gray>”);
b.append(shortKey(haveKey.get(0).key));
b.append(“</font></b>: &nbsp; <br> &nbsp; <font size=+0>”); // odd — this still makes it bigger
appendBold(b, haveKey.get(0).value);
b.append(“</font> &nbsp; “); // to align, nbsp’s must happen at same font size
return;
}

if (haveKey.size() > 0) {
b.append(“<b>”);
for (VueMetadataElement md : haveKey) {
if (DEBUG.Enabled) b.append(NEWLINE); // newline for debugging — watch whitespace impact
b.append(“&nbsp;<font color=gray>”);
b.append(truncate(shortKey(md.key)));
b.append(“:</font> “);
b.append(truncate(md.value));
b.append(“&nbsp;<br>”);
}
b.append(“</b>”);
}

if (haveOnlyValue.size() > 0) {
for (VueMetadataElement md : haveOnlyValue) {
if (DEBUG.Enabled) b.append(NEWLINE); // newline for debugging — watch whitespace impact
b.append(“&nbsp;&thinsp;&bull; “);
appendBold(b, truncate(md.value));
b.append(” &nbsp; <br>”);
}
}

if (haveKey.size() + haveOnlyValue.size() > 0)
return;

// Below is old complex code that should just be dumped, but it works okay as a failsafe to
// handle case of only having key(s) that have no value;

final boolean isSingle = (dataList.size() == 1);
for (VueMetadataElement md : dataList) {
if (md.type != typeRequest)
continue;
if (DEBUG.Enabled) b.append(NEWLINE); // newline only for debugging the output string
final boolean hasKey = !(md.key == null || md.key == ONTOLOGY_NONE || md.key == KEY_TAG);

b.append(“&nbsp;”);
if (isSingle)
b.append(“<font size=+0>”); // odd — this still makes it bigger
else if (hasKey)
b.append(“<font color=gray>”);

if (hasKey)
b.append(shortKey(md.key)).append(‘:’);
else
b.append(“&bull;”);
if (hasKey && !isSingle) b.append(“</font>”);
b.append(SPACE);
if (md.value != null) {
if (!hasKey || isSingle)
appendBold(b, truncate(md.value));
else
b.append(truncate(md.value));
}
b.append(“&nbsp;<br>”);
}
}

private void buildHTMLForMergeSources(final StringBuilder b)
{
for (VueMetadataElement md : dataList) {
if (md.type == OTHER || md.type == TAG) {
if (md.value == null)
continue;
b.append(“<br>&nbsp;”);
final int startLine = b.length();
final String[] part;
if (md.value != null && md.value.startsWith(“source:”)) {
// handle old style pre-summer-2012 merge-map meta-data annotations
part = md.value.substring(8).split(“,”); // format: map-file-name,label
b.append(“&thinsp;&bull; “);
if (part.length > 1) {
b.append(part[0]).append(SLASH);
appendItalic(b, truncate(part[1].trim()));
}
} else {
part = md.value.split(“/”); // format: ID/typeChar/map-file-name/label
if (part.length > 3) {
if (part[1].charAt(0) == ‘L’) // link (also: N=node, I=image, etc)
b.append(“&harr; “); // horizontal double-ended arrow
else
b.append(“&thinsp;&bull; “); // bullet: align under ‘M’ of “Merge sources”
b.append(part[2]).append(SLASH); // map name
appendItalic(b, truncate(part[3])); // node/component name
}
}
if (b.length() == startLine)
b.append(md.value); // dump all if didn’t parse
b.append(“&nbsp;”);
}
}
}

private static StringBuilder appendItalic(StringBuilder b, String s) {
return b.append(“<i>”).append(s).append(“</i>”);
}
private static StringBuilder appendBold(StringBuilder b, String s) {
return b.append(“<b>”).append(s).append(“</b>”);
}

private static String shortKey(String k) {
final int localPart = k.indexOf(‘#’);
if (localPart > 1 && localPart < (k.length() – 1))
return k.substring(localPart + 1);
else
return k;
}
private static String truncate(String s) { return truncate(s, 50); }
private static String truncate(String s, final int max) {
// Re: truncation: Notes rollover wraps text via JTextArea, which won’t give us HTML. If
// we want text wrapping, we may be able to do it using a table.
if (s.length() > max)
return s.substring(0,max) + “&hellip;”;
else
return s;
}

private StringBuilder getOldMetadataHTML(int type)
{
final StringBuilder b = new StringBuilder();
final SubsetList mdList;

if(type == VueMetadataElement.CATEGORY)
mdList = getCategoryList();
else if(type == VueMetadataElement.ONTO_TYPE)
mdList = getOntologyList();
else
mdList = getOtherList();

final int size = mdList.size();

if (size <= 0)
return b;

for (int i = 0; i < size; i++) {
final String value = mdList.get(i).getValue();

if (DEBUG_LOCAL) Log.debug(“HTML loop — value for ” + i + ” type: ” + type + ” value: ” + value);

if (value == null) {
if (DEBUG.MEGA) Log.debug(“GET-AS-HTML: null meta-data value for ” + mdList.get(i));
//Util.printStackTrace();
continue;
}

if (value.length() > 0) {
b.append(“<br>”);
if (type == VueMetadataElement.ONTO_TYPE) {
final int nameLocation = value.indexOf(VueMetadataElement.ONT_SEPARATOR);
if(nameLocation > -1 && value.length() > nameLocation + 1) {
b.append(value, nameLocation + 1, value.length()); // todo check: off by 1 anywhere?
//value = value.substring(nameLocation + 1);
} else
b.append(value);
}
else if(type == VueMetadataElement.OTHER) {
b.append(” &nbsp; “);
// This looks like it’s expecting a specific format from somewhere…
final int colon = value.indexOf(“:”);
String cursor = value;
// Based on indention, doesn’t look like that semicolon was on purpose — so this never worked right.
// Tho again, this is another bug I’m afraid to fix as something else in this spaghetti fest might be relying on it…
//if (cLocation > -1 && value.length() > cLocation + 1);
// value = value.substring(cLocation + 1);
if (colon > -1 && value.length() > colon + 1)
cursor = value.substring(colon + 1);
final int dot = cursor.lastIndexOf(“.”);
if (dot != -1) {
final int comma = cursor.indexOf(“,”);
String endPart = “”;
if (comma != -1 && comma > dot) {
endPart = cursor.substring(comma); // todo: factor down to append
}
cursor = cursor.substring(0,dot) + endPart;
}
b.append(cursor);
} else
b.append(value);
}
}

if(b.length() > 0) {
if(type == CATEGORY)
b.insert(0, “Keywords: “);
else if(type == ONTO_TYPE)
b.insert(0, “Ontological Membership: “);
else if(type == OTHER)
b.insert(0, “Merged from: “);
// So OTHER meta-data is, in fact, hardcoded for the purpose of merge-sources.
}
return b;
}

public SubsetList getCategoryList() { return categoryList; }
public SubsetList getOntologyList() { return ontologyList; }
public SubsetList getOtherList() { return otherList; }

/** a decorator for the main list that makes it look like it only contains the given type */
public class SubsetList
{
private final int type;
public SubsetList(int type) { this.type = type; }

private int getSizeForNonResourceType(final int t) {
// [DAN] “not yet needed — also add in contains..”
//if(type == VueMetadataElement.RESOURCE_CATEGORY)
// size = getResourceListSize();

if (t == VueMetadataElement.RESOURCE_CATEGORY) {
Log.warn(“impl never searched RESOURCE_CATEGORY”);
return 0;
}
if (t == VueMetadataElement.CATEGORY)
return getCategoryListSize();
else if (t == VueMetadataElement.ONTO_TYPE)
return getOntologyListSize();
else
return getOtherListSize();
}

public int indexOf(VueMetadataElement vme) {
final int size = getSizeForNonResourceType(type);
for (int i=0; i<size; i++) {
if (type == VueMetadataElement.CATEGORY && getCategoryListElement(i).equals(vme))
return i;
else if (type == VueMetadataElement.ONTO_TYPE && getOntologyListElement(i).equals(vme))
return i;
else if (getOtherListElement(i).equals(vme))
return i;
}
// Looks like the blocks were screwed up — could only ever find CATEGORY
// — afraid to actually fix this…
// for(int i=0;i<size;i++) {
// if(type == VueMetadataElement.CATEGORY)
// if(getCategoryListElement(i).equals(vme))
// return i;
// else if(type == VueMetadataElement.ONTO_TYPE)
// if(getOntologyListElement(i).equals(vme))
// return i;
// else
// if(getOtherListElement(i).equals(vme))
// return i;
// }
//note: currently resource categories are not found.
return -1;
}

public boolean contains(VueMetadataElement vme) {
final int size = getSizeForNonResourceType(type);

for(int i=0; i<size; i++) {
if (type == VueMetadataElement.CATEGORY && getCategoryListElement(i).equals(vme))
return true;
else if (type == VueMetadataElement.ONTO_TYPE && getOntologyListElement(i).equals(vme))
return true;
else if (getOtherListElement(i).equals(vme))
return true;
}
// More screwed up blocking… again, afraid to fix this…
// for(int i=0;i<size;i++) {
// if(type == VueMetadataElement.CATEGORY)
// if(getCategoryListElement(i).equals(vme))
// return true;
// else if(type == VueMetadataElement.ONTO_TYPE)
// if(getOntologyListElement(i).equals(vme))
// return true;
// else
// if(getOtherListElement(i).equals(vme))
// return true;
// }
// note: as in indexOf() currently resouerce categories are not found.
return false;
}

public void add(VueMetadataElement vme) {
((CategoryFirstList)getMetadata()).add(vme);
}

public int size() { return sizeForType(type); }

public VueMetadataElement get(int i)
{
if (type == VueMetadataElement.RESOURCE_CATEGORY)
return getResourceListElement(i);
else if(type == VueMetadataElement.CATEGORY)
return getCategoryListElement(i);
else if(type == VueMetadataElement.ONTO_TYPE)
return getOntologyListElement(i);
else
return getOtherListElement(i);
}

// Used in MetadataEdtior.java
public void set(int i,VueMetadataElement ele)
{
if (type == VueMetadataElement.RESOURCE_CATEGORY)
setResourceListElement(i,ele);
else if(type == VueMetadataElement.CATEGORY)
setCategoryListElement(i,ele);
else if(type == VueMetadataElement.ONTO_TYPE)
setOntologyListElement(i,ele);
else
setOtherListElement(i,ele);
}

public java.util.List getAsList()
{
final java.util.List returnList = new java.util.ArrayList();
final int size = size();
for(int i=0; i < size; i++)
returnList.add(get(i));
return returnList;
}
}

private class CategoryFirstList extends java.util.ArrayList<VueMetadataElement>
{
//private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(CategoryFirstList.class);

private int categoryEndIndex = 0;
private int ontologyEndIndex = 0;
private int otherEndIndex = 0;
private int rCategoryEndIndex = 0; // RESOURCE_CATEGORY

public int getCategoryEndIndex() { return categoryEndIndex; }
private int getResourceEndIndex() { return rCategoryEndIndex; }
private int getOntologyEndIndex() { return ontologyEndIndex; }
private int getOtherEndIndex() { return otherEndIndex; }

/** called via BOTH castor restore as well as runtime API */
@Override public boolean add(final VueMetadataElement vme)
{
if (vme.type == VME_EMPTY_IGNORE) {
if (!unmarshalling) Log.info(“ignoring ” + vme);
return true;
}

if (vme.getObject() == null) {
if (DEBUG_LOCAL) Log.debug(“VME object is null, (re)setting type…” + vme.getType());
vme.setType(vme.getType()); // ick ick ick
}

if(DEBUG_LOCAL)
Log.debug(“categoryFirstList add – catEnd,ontEnd,size(): ”
+ categoryEndIndex + “,” + ontologyEndIndex + “,” + size());

if (vme.type == VueMetadataElement.ONTO_TYPE && vme.getObject() instanceof OntType) {
otherEndIndex++;
super.add(ontologyEndIndex++, vme);
}
else if (vme.type == VueMetadataElement.RESOURCE_CATEGORY && vme.getObject() instanceof String[]) {
ontologyEndIndex++;
categoryEndIndex++;
otherEndIndex++;
super.add(rCategoryEndIndex++, vme);
if(DEBUG_LOCAL) Log.debug(“rCategoryIndex is now: ” + rCategoryEndIndex);
}
else if (vme.type == VueMetadataElement.CATEGORY && vme.getObject() instanceof String[]) {
ontologyEndIndex++;
otherEndIndex++;
super.add(categoryEndIndex++, vme);
}
else {
// WARNING: checks for getObject mean possible for a NON-OTHER type to wind up in other sub-list
super.add(otherEndIndex++, vme);
}

// MAJOR ISSUE: THIS WAS FIRING DURING DESERIALIZATION, TO A GLOBALLY *STATIC* LIST OF
// LISTENERS, WHICH MEANS CALLS TO AWT CODE FOR EVERY SINGLE VME IN ANY NODE IN ANY
// MAP. Including one of the worse AWT calls: revalidate(). This could also sometimes
// cause AWT hangs, often in somewhere down in cursor code(?) We’ve now reduced all
// MDL update events to diagnostics only — turns out there’s no place in VUE where
// there are AWT components watching meta-data that may change outside the UI changing
// them.
fireListChanged();
return true;
}

/** We must override the default object remover to force use our indexed remover to keep
* the internal indices correct */
@Override public boolean remove(Object vme) {
// throw new UnsupportedOperationException(“remove: ” + o + “; must remove by index”);
return remove(indexOf(vme)) != null;
}

@Override public VueMetadataElement remove(final int i)
{
if (i < 0) {
Log.warn(“remove: index = ” + i);
return null;
}

// Attempt the remove 1st so that if we get a RangeCheck exception,
// we haven’t adjusted our indicies.
final VueMetadataElement removed = super.remove(i);

if (i < rCategoryEndIndex) {
rCategoryEndIndex–;
if (categoryEndIndex > 0) ontologyEndIndex–;
if (ontologyEndIndex > 0) ontologyEndIndex–;
if (otherEndIndex > 0) otherEndIndex–;
}
if (i < categoryEndIndex && i >= rCategoryEndIndex) {
categoryEndIndex–;
if (ontologyEndIndex > 0) ontologyEndIndex–;
if (otherEndIndex > 0) otherEndIndex–;
}
else if (i >= categoryEndIndex && i < ontologyEndIndex) {
ontologyEndIndex–;
otherEndIndex–;
}
else
otherEndIndex–;

fireListChanged();

return removed;
}
}

public static void addListener(MetadataListListener listener) {
// Util.printStackTrace(MetadataList.class + “:STATIC:addListener:IGNORED ” + Util.tags(listener));
if (DEBUG.Enabled) Log.debug(“listeners no longer allowed, will not report to: ” + Util.tags(listener));
// listeners.add(listener);
}

// Oops — this disable flag was static, which means meta-data list events from other threads
// could tromp all over the updates from other threads (not that we really need this update at
// all, fortunately)
// private static boolean disableEvents = false;
private static void fireListChanged()
{
// if (DEBUG.Enabled) Log.info(“fireListChanged: ” + Util.tags(tag));

// if (disableEvents)
// return;
// for (MetadataListListener mdl : listeners) {
// try {
// mdl.listChanged();
// } catch (Throwable t) {
// Log.warn(“listener update: ” + Util.tags(mdl), t);
// }
// }
}

public interface MetadataListListener { void listChanged(); }
}