/* * 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.vue.DEBUG; import tufts.Util; import tufts.vue.EventHandler; import static tufts.vue.ds.Relation.*; import java.util.*; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; /** * A list of relationships between Fields. Each association names two Field's, * which must be from different Schema's. How VUE makes meaning of these associations * is generally determined elsewhere, with the exeption of an Association between * the key field of two Schema's, which is considered to be a join in the classic * database sense. * * @version $Revision: 1.16 $ / $Date: 2010-02-03 19:13:16 $ / $Author: mike $ * @author Scott Fraize */ // bug: associations are not persisting unless at least one node using the association is on the map public final class Association { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(Association.class); private static final Multimap<Field,Association> AllByField = Multimaps.newHashMultimap(); private static final List<Association> AllPairsList = new ArrayList(); final Field field1; final Field field2; final Schema schema1; final Schema schema2; boolean enabled; public static final class Event { public static final Object ADDED = "added"; public static final Object REMOVED = "removed"; public static final Object CHANGED = "changed"; final Object id; final Association a; private Event(Association a, Object id) { this.a = a; this.id = id; } @Override public String toString() { return String.format("%s[%s%s]", Util.tag(this), id, a == null ? "" : (" " + a)); } } public interface Listener extends EventHandler.Listener<Event> {} private static final EventHandler<Event> EventSource = EventHandler.getHandler(Event.class); private Association(Field f1, Field f2, boolean isOn) { if (f1 == f2) throw new IllegalArgumentException("field can't associate to itself: " + f1); if (f1 == null || f2 == null) throw new IllegalArgumentException("null field: " + f1 + "; " + f2); field1 = f1; field2 = f2; schema1 = field1.getSchema(); schema2 = field2.getSchema(); if (DEBUG.SCHEMA) Log.debug("instancing:" + "\n\tfield 1: " + quoteKey(f1) + " " + Util.tags(f1) + " " + schema1 + "\n\tfield 2: " + quoteKey(f2) + " " + Util.tags(f2) + " " + schema2); enabled = isOn; if (schema1 == schema2) throw new IllegalArgumentException("can't associate fields from the same schema: " + f1 + "; " + f2 + "; " + schema1); } private static final int INDEX_DEFAULT = -1; /** add a global association between the given two fields */ public static void add(Field f1, Field f2) { Association a = addImpl(f1, f2, INDEX_DEFAULT); if (a != null) EventSource.raise(Association.class, new Event(a, Event.ADDED)); } private static synchronized Association addImpl(Field f1, Field f2, int index) { boolean alreadyExists = false; for (Association scan : AllPairsList) { if ((scan.field1 == f1 && scan.field2 == f2) || (scan.field1 == f2 && scan.field2 == f1)) { alreadyExists = true; if (DEBUG.SCHEMA) Log.debug("an association already exists between " + f1 + " and " + f2); break; } } if (alreadyExists) { return null; } else { Association a = new Association(f1, f2, true); if (DEBUG.SCHEMA) Log.debug("ADDING " + a + " at index " + index); AllByField.put(f1, a); AllByField.put(f2, a); if (index >= 0) AllPairsList.add(index, a); else AllPairsList.add(a); if (DEBUG.SCHEMA) { Log.debug("all associations:"); Util.dump(AllPairsList); } return a; } } public static void remove(Association a) { removeImpl(a); EventSource.raise(Association.class, new Event(a, Event.REMOVED)); } private static synchronized void removeImpl(Association a) { if (DEBUG.SCHEMA) Log.debug("REMOVE " + a); AllPairsList.remove(a); AllByField.remove(a.getLeft(), a); AllByField.remove(a.getRight(), a); } static void updateForNewAuthoritativeSchema(final Schema newAuthority) { if (verifyAll(newAuthority)) EventSource.raise(Association.class, new Event(null, Event.CHANGED)); } private static synchronized boolean verifyAll(final Schema newAuthority) { boolean changed = false; for (Association a : Util.copy(AllPairsList)) { // list may change during iteration // Technically, we should only really need to look for Associations that // contain Schema's that can be replaced by newAuthority. We could also // just scan for Schema's that have been discarded, which we shouldn't be // seeing any of in the global list. But just in case, we end up doing all // of the above and just scan every association for conditions that don't // appear to be in sync. try { if (verifyAndUpdate(a, newAuthority)) changed = true; } catch (Throwable t) { Log.error("verifying association " + Util.tags(a) + " with newAuthority " + newAuthority, t); } } return changed; } private static boolean verifyAndUpdate(final Association a, final Schema newAuthority) { final Schema s1 = replaceSchema(a, a.schema1, newAuthority); final Schema s2 = replaceSchema(a, a.schema2, newAuthority); if (s1 == a.schema1 && s2 == a.schema2) { // doesn't need updating return false; } final Field f1, f2; if (s1 == a.schema1) f1 = a.field1; else f1 = replaceField(a.field1, s1); if (s2 == a.schema2) f2 = a.field2; else f2 = replaceField(a.field2, s2); final Association replacement; synchronized (Association.class) { // todo: we want to replace the exact location in the AllPairsList // so the UI won't re-order int index = AllPairsList.indexOf(a); removeImpl(a); // toss the old one completely a.enabled = false; // just in case, tho nothing should be referencing replacement = addImpl(f1, f2, index); } return true; //EventSource.raise(Association.class, new Event(replacement, Event.CHANGED)); } private static Schema replaceSchema(Association a, final Schema old, final Schema newAuthority) { final Schema s = Schema.lookupAuthority(old); if (s != old) { if (s != newAuthority) { Log.warn("ASSOCIATION NEEDED PATCHING UNRELATED TO NEW AUTHORITY REPORT;" + "\n\t old: " + old + "\n\t lookup: " + s + "\n\tnewAuth: " + newAuthority + "\n\t assoc: " + a ,new Throwable("HERE")); } if (!old.isDiscarded()) { Log.warn("REPLACED SCHEMA WASN'T DISCARDED; " + "\n\t old: " + old + "\n\t lookup: " + s + "\n\tnewAuth: " + newAuthority + "\n\t assoc: " + a ,new Throwable("HERE")); } } return s; } private static Field replaceField(Field oldField, Schema newSchema) { return newSchema.getField(oldField.getName()); } // public static void addByAll(Field newField, Collection<Field> existingFields) { // if (existingFields.size() == 0) // return; // for (Field f : existingFields) // new Association(newField, f, false); // // if (DEBUG.Enabled) { // // Log.debug("CURRENT ASSOC:"); // // for (Association a : AllPairsList) { // // System.out.println("\t" + a); // // } // // } // EventSource.raise(newField, new Event(null, Event.ADDED)); // } /** @return all Associations */ public static List<Association> getAll() { return AllPairsList; } public static int getCount() { return AllPairsList.size(); } public static Association get(int n) { if (n < 0 || n > getCount()) return null; else return AllPairsList.get(n); } /** @return All enabled associations between the two given schemas */ public static List<Association> getBetweens(Schema s1, Schema s2) { final List<Association> betweens = new ArrayList(); for (Association a : getAll()) { if (a.isEnabled() && a.isBetween(s1, s2)) betweens.add(a); } //if (DEBUG.SCHEMA && DEBUG.META) if (DEBUG.Enabled) dumpSmart(betweens, "betweens for schemas: " + s1.getName() + " x " + s2.getName()); return betweens; } /** @return all enabled JOIN's between the two given schemas given the field of interest * A JOIN is an any association that does NOT involve the given Field. */ public static List<Association> getJoins(Schema s1, Field field) { final List<Association> betweens = new ArrayList(); final Schema s2 = field.getSchema(); if (s1 == null) throw new NullPointerException("null Schema"); if (field == null) throw new NullPointerException("null Field"); for (Association a : getAll()) { if (a.isEnabled() && a.isBetween(s1, s2) && !a.contains(field)) betweens.add(a); } if (DEBUG.Enabled) { dumpSmart(betweens, "joins for schemas: " + s1.getName() + " x " + s2.getName() + "; excluding-field: " + Relation.quoteKey(field)); } return betweens; } private static void dumpSmart(List list, String msg) { String s = ""; if (list.size() <= 1) { if (list.size() == 1) s = ": " + list.get(0).toString(); else s = ": none"; //s = ": " + Util.tags(list); } Log.debug(msg + s); if (list.size() > 1) Util.dump(list); } /** @return true if there are any associations between the two schemas that do NOT contain the given Field, * which means we'll need to do a join-extract to look for relationships */ public static boolean hasJoins(Schema s1, Field field) { final Schema s2 = field.getSchema(); for (Association a : getAll()) if (a.isEnabled() && a.isBetween(s1, s2) && !a.contains(field)) return true; return false; } // public static boolean hasBetweens(Schema s1, Schema s2) { // for (Association a : getAll()) // if (a.isEnabled() && a.isBetween(s1, s2)) // return true; // return false; // } /** @return all associations containing the given Field as one side of the association */ public static Collection<Association> getAliases(Field f) { final Collection<Association> got = AllByField.get(f); if (DEBUG.Enabled) { final Object gots; if (got.size() == 0) gots = null; else if (got.size() == 1) gots = Util.getFirst(got); else gots = ""; //Log.debug("found aliases for " + f + ": " + gots); if (got.size() > 1) { Util.dump(got); Log.debug("found aliases for " + f + ": " + gots); } } return got; } /** @return all opposite Fields mentioned in associations containing the given Field */ public static Collection<Field> getPairedFields(Field field) { final Collection<Field> pairedTo = new HashSet(); for (Association a : Association.getAliases(field)) { pairedTo.add(a.getPairedField(field)); } return pairedTo; } public boolean contains(Field f) { return f == field1 || f == field2; } public Field getLeft() { return field1; } public Field getRight() { return field2; } public Field getPairedField(Field f) { if (f == field1) return field2; else if (f == field2) return field1; else throw new Error("field not in association: " + f); } public Field getFieldForSchema(Schema s) { if (s == schema1) return field1; else if (s == schema2) return field2; else throw new Error("field for schema not in association: " + s); } public String getKeyForSchema(Schema s) { return getFieldForSchema(s).getName(); } public boolean isBetween(Schema s1, Schema s2) { if (schema1 == s1 && schema2 == s2) return true; else if (schema1 == s2 && schema2 == s1) return true; else return false; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean on) { enabled = on; } // /** @return true if the two Schema's are joined -- their key fields are associated */ // public static boolean isJoined(Schema s1, Schema s2) { // return getJoin(s1, s2) != null; // } // /** @return true if this association is a DOUBLE JOIN -- an association between // * the key fields of two schemas. // */ // public boolean isDoubleJoin() { // return schema1.getKeyField() == field1 // && schema2.getKeyField() == field2; // } // /** @return true if this association is a JOIN -- an association between // * at least one key field of one of the Schema's // */ // public boolean isJoin() { // return schema1.getKeyField() == field1 // || schema2.getKeyField() == field2; // } // /** @return the Association found if the two Schema's are joined (their key fields are associated) */ // public static Association getJoin(Schema s1, Schema s2) { // if (s1 == s2) // return null; // // if we need more performance here, maintain this via a special map so we // // can instantly look it up -- or even store it right in each Schema, which // // would probably be the fastest. We'll need to handle updating those maps // // tho for Schema's that reload after a user has, god forbid change their // // key field. // for (Association a : getAll()) // if (a.isEnabled() && a.isBetween(s1, s2) && a.isJoin()) // return a; // return null; // } @Override public String toString() { return String.format("Association[%s%s=%s]", enabled ? "" : "DISABLED ", quoteKey(field1), quoteKey(field2)); } }