package org.archstudio.bna.things; import static com.google.common.base.Preconditions.checkState; import static org.archstudio.sysutils.SystemUtils.simpleName; import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import org.archstudio.bna.IThing; import org.archstudio.bna.IThingListener; import org.archstudio.bna.ThingEvent; import org.archstudio.bna.keys.IThingKey; import org.archstudio.bna.utils.BNAUtils; import org.archstudio.sysutils.FastMap; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import com.google.common.base.Function; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; @NonNullByDefault public abstract class AbstractThing implements IThing { private final Object id; private final int hashCode; private final FastMap<IThingKey<?>, Object> properties = new FastMap<>(true); private final FastMap<IThingKey<?>, Object> shapeModifyingKeys = new FastMap<>(true); private boolean initialized; public AbstractThing(@Nullable Object id) { this.id = id != null ? id : new Object(); hashCode = this.id.hashCode(); initProperties(); initialized = true; } @Override public final Object getID() { return id; } @Override public int hashCode() { return hashCode; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } AbstractThing other = (AbstractThing) obj; if (id == null) { if (other.id != null) { return false; } } else if (id != other.id && !id.equals(other.id)) { return false; } return true; } protected void initProperties() { } protected final <V> void initProperty(IThingKey<V> key, @Nullable V value) { checkState(!initialized, "Method may only be called from initProperties()"); properties.put(key, value); } protected void addShapeModifyingKey(IThingKey<?> key) { checkState(!initialized, "Method may only be called from initProperties()"); shapeModifyingKeys.put(key, null); } protected void removeShapeModifyingKey(IThingKey<?> key) { checkState(!initialized, "Method may only be called from initProperties()"); shapeModifyingKeys.remove(key); } @Override public boolean isShapeModifyingKey(IThingKey<?> key) { return shapeModifyingKeys.containsKey(key); } private final CopyOnWriteArrayList<IThingListener> thingListeners = Lists.newCopyOnWriteArrayList(); @Override public final void addThingListener(IThingListener thingListener) { thingListeners.add(thingListener); } @Override public final void removeThingListener(IThingListener thingListener) { thingListeners.remove(thingListener); } private final void fireThingEvent(ThingEvent event) { for (IThingListener l : thingListeners) { try { l.thingChanged(event); } catch (Exception e) { e.printStackTrace(); } } } @Override public final @Nullable <V> V get(IThingKey<V> key) { return get(key, null); } @SuppressWarnings("unchecked") @Override public final <V> V get(IThingKey<V> key, V valueIfNull) { BNAUtils.checkLock(); V value = (V) properties.get(key); return value != null ? key.clone(value) : valueIfNull; } @Override public final boolean has(IThingKey<?> key) { BNAUtils.checkLock(); return properties.containsKey(key); } @Override public final <V> boolean has(IThingKey<V> key, @Nullable V value) { BNAUtils.checkLock(); Map.Entry<IThingKey<?>, Object> entry = properties.getEntry(key); if (entry != null) { return BNAUtils.like(entry.getValue(), value); } return false; } @SuppressWarnings("unchecked") @Override public final <V> V set(IThingKey<V> key, @Nullable V value) { BNAUtils.checkLock(); if (!key.isNullable() && value == null) { throw new NullPointerException(key.toString()); } V oldValue = value; Map.Entry<IThingKey<?>, Object> entry = properties.createEntry(key); if (!BNAUtils.like(entry.getValue(), value)) { oldValue = (V) entry.getValue(); entry.setValue(key.clone(value)); fireThingEvent(ThingEvent.create(ThingEvent.EventType.PROPERTY_SET, this, key, oldValue, value)); } return oldValue; } @SuppressWarnings("unchecked") @Override public final @Nullable <V> V remove(IThingKey<V> key) { BNAUtils.checkLock(); V oldValue = null; Map.Entry<IThingKey<?>, Object> entry = properties.removeEntry(key); if (entry != null) { oldValue = (V) entry.getValue(); fireThingEvent(ThingEvent.create(ThingEvent.EventType.PROPERTY_REMOVED, this, key, entry.getValue(), null)); } return oldValue; } @Override public Set<Map.Entry<IThingKey<?>, ?>> entrySet() { BNAUtils.checkLock(); return Collections.unmodifiableSet(Sets.newHashSet(Iterables.transform(properties.entrySet(), new Function<Map.Entry<IThingKey<?>, Object>, Map.Entry<IThingKey<?>, ?>>() { @Override public Map.Entry<IThingKey<?>, ?> apply(Map.Entry<IThingKey<?>, Object> input) { final IThingKey<?> key = input.getKey(); final Object value = input.getValue(); return new Map.Entry<IThingKey<?>, Object>() { @Override public IThingKey<?> getKey() { return key; } @Override public Object getValue() { return value; } @Override public Object setValue(Object value) { throw new UnsupportedOperationException(); } @Override public String toString() { return key + " = " + value; } }; } }))); } @Override public String toString() { BNAUtils.checkLock(); StringBuffer sb = new StringBuffer(); sb.append(simpleName(this.getClass())).append(" (").append(id).append("):\n").append(properties.toString()); return sb.toString(); } }