/*
* Copyright (C) 2011 Andrea Schweer
*
* This file is part of the Digital Parrot.
*
* The Digital Parrot is free software; you can redistribute it and/or modify
* it under the terms of the Eclipse Public License as published by the Eclipse
* Foundation or its Agreement Steward, either version 1.0 of the License, or
* (at your option) any later version.
*
* The Digital Parrot is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the Eclipse Public License for
* more details.
*
* You should have received a copy of the Eclipse Public License along with the
* Digital Parrot. If not, see http://www.eclipse.org/legal/epl-v10.html.
*
*/
package net.schweerelos.parrot.model.filters;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.log4j.Logger;
import net.schweerelos.parrot.model.NodeWrapper;
import net.schweerelos.parrot.model.ParrotModel;
public class Chain {
public static final String LAST_LINK_PROPERTY = "LAST_NODE_PROPERTY";
public static final String SIZE_PROPERTY = "SIZE_PROPERTY";
public static final String CONTENTS_PROPERTY = "CONTENTS_PROPERTY";
private ParrotModel model;
private PropertyChangeSupport changeSupport;
private List<ChainLink> chain;
private List<List<NodeWrapper>> nodeChains;
private PropertyChangeListener lastLinkListener;
private PropertyChangeListener secondLastLinkListener;
private PropertyChangeListener anyLinkChangeListener;
public Chain(ParrotModel model) {
this.model = model;
chain = new ArrayList<ChainLink>();
changeSupport = new PropertyChangeSupport(this);
lastLinkListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent pce) {
if (!pce.getSource().equals(getLastLink())) {
return;
}
String propName = pce.getPropertyName();
boolean addAnyAny = false;
if (propName.equals(ChainLink.INSTANCE_PROPERTY)) {
// do we need to add a new link?
addAnyAny = pce.getOldValue() == null // if the instance
// used to be 'any'
&& pce.getNewValue() != null // but isn't 'any' now
&& !getLastLink().hasType(); // and there is no type
} else if (propName.equals(ChainLink.TYPE_PROPERTY)) {
// same as above (just type/instance reversed)
addAnyAny = pce.getOldValue() == null
&& pce.getNewValue() != null
&& !getLastLink().hasInstance();
}
if (addAnyAny) {
// don't automatically do this
// add(null, null);
}
}
};
secondLastLinkListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent pce) {
ChainLink secondLastLink = getSecondLastLink();
if (!pce.getSource().equals(secondLastLink)) {
return;
}
if (!secondLastLink.hasInstance() && !secondLastLink.hasType()) {
remove(getLastLink());
}
}
};
anyLinkChangeListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent pce) {
String propName = pce.getPropertyName();
if (propName.equals(ChainLink.INSTANCE_PROPERTY)
|| propName.equals(ChainLink.TYPE_PROPERTY)) {
Object source = pce.getSource();
int index = chain.indexOf(source);
changeSupport.fireIndexedPropertyChange(CONTENTS_PROPERTY,
index, null, source);
}
}
};
addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
// if anything changes, reset cached nodeChains
nodeChains = null;
}
});
add(null, null);
}
public void expandOrRestart(NodeWrapper node) {
if (chain.isEmpty()) {
add(null, node);
return;
}
ChainLink lastLink = getLastLink();
if (lastLink.hasInstance() && lastLink.getInstance().equals(node)) {
// add new any/any
add(null, null);
return;
}
ChainLink chainLink = getChainLink(node);
int index = chain.indexOf(chainLink);
if (index < chain.size() - 1) {
if (chainLink.hasType()) {
// if it used to be a type link, make it an instance link instead
chainLink.setInstance(node);
}
// this node is "in" the middle of the chain somewhere
// -> re-start the chain at that instance
ChainLink oldLast = getLastLink();
int oldSize = chain.size();
chain = chain.subList(index, chain.size());
changeSupport.firePropertyChange(LAST_LINK_PROPERTY, oldLast, getLastLink());
changeSupport.firePropertyChange(SIZE_PROPERTY, oldSize, chain.size());
return;
} else {
// must be the last chain link then
// set the last link's instance to node
getLastLink().setInstance(node);
}
}
private ChainLink getChainLink(NodeWrapper node) {
ChainLink lastLink = getLastLink();
if (chain.size() == 1 && !lastLink.hasType() && !lastLink.hasInstance()) {
return lastLink;
}
if (nodeChains == null) {
nodeChains = model.getChains(chain);
}
for (int i = 0; i < chain.size(); i++) {
ChainLink currentLink = chain.get(i);
for (List<NodeWrapper> chain : nodeChains) {
NodeWrapper chainWrapper = chain.get(i);
if (chainWrapper.equals(node)) {
return currentLink;
}
}
}
// TODO Auto-generated method stub
return null;
}
public Set<NodeWrapper> getPossibleTypes(ChainLink link) {
if (link.hasInstance()) {
return getPossibleTypesForIndividual(link.getInstance());
}
int index = chain.indexOf(link);
if (index == 0) {
// start of chain -> everything is possible
return model.getSubjectTypes();
}
Set<NodeWrapper> result = new HashSet<NodeWrapper>();
if (nodeChains == null) {
nodeChains = model.getChains(chain);
}
for (List<NodeWrapper> chain : nodeChains) {
if (chain.size() > index) {
NodeWrapper chainWrapper = chain.get(index);
result.addAll(model.getTypesForIndividual(chainWrapper));
} else {
Logger logger = Logger.getLogger(Chain.class);
logger.warn("unexpectedly short chain -- was expecting "
+ (index + 1) + ", is " + (chain.size()));
}
}
return result;
}
public Set<NodeWrapper> getPossibleTypesForIndividual(NodeWrapper instance) {
return model.getTypesForIndividual(instance);
}
public Set<NodeWrapper> getPossibleInstancesForType(ChainLink link) {
if (!link.hasType()) {
return Collections.emptySet();
}
NodeWrapper type = link.getType();
int index = chain.indexOf(link);
if (index == 0) {
return model.getIndividualsForType(type);
}
Set<NodeWrapper> result = new HashSet<NodeWrapper>();
if (nodeChains == null) {
nodeChains = model.getChains(chain);
}
for (List<NodeWrapper> chain : nodeChains) {
NodeWrapper chainWrapper = chain.get(index);
Set<NodeWrapper> wrapperTypes = model.getTypesForIndividual(chainWrapper);
if (wrapperTypes.contains(type)) {
result.add(chainWrapper);
}
}
return result;
}
public ChainLink getLastLink() {
if (chain.size() < 1) {
return null;
}
return chain.get(chain.size() - 1);
}
private ChainLink getSecondLastLink() {
if (chain.size() < 2) {
return null;
}
return chain.get(chain.size() - 2);
}
/**
* The filter is empty in one of two cases:
* <ul>
* <li>it has no links at all (which can't really happen, but hey); or</li>
* <li>it has exactly one link, and this link is an "any/any" link
* (ie {@code !getLastLink().hasInstance() && !getLastLink().hasType()})</li>
* </ul>
*/
public boolean isEmpty() {
if (chain.isEmpty()) {
return true;
}
if (chain.size() > 1) {
return false;
}
return !getLastLink().hasInstance() && !getLastLink().hasType();
}
private boolean isLastLink(ChainLink link) {
if (chain.isEmpty()) {
return false;
}
return getLastLink().equals(link);
}
private boolean isSecondLastLink(ChainLink link) {
if (chain.size() < 2) {
return false;
}
return chain.get(chain.size() - 2).equals(link);
}
/**
* Adds a new chain link to the chain, with the specified type and instance
* (both of which can be {@code null}).
*
* @param type
* @param instance
*/
public void add(NodeWrapper type, NodeWrapper instance) {
ChainLink oldLast = getLastLink();
ChainLink oldSecondLast = getSecondLastLink();
int oldSize = chain.size();
ChainLink chainLink = new ChainLink(type, instance);
chain.add(chainLink);
chainLink.addPropertyChangeListener(anyLinkChangeListener);
int newSize = chain.size();
if (oldLast != null) {
oldLast.removePropertyChangeListener(lastLinkListener);
}
chainLink.addPropertyChangeListener(lastLinkListener);
if (oldSecondLast != null) {
oldSecondLast.removePropertyChangeListener(secondLastLinkListener);
}
ChainLink newSecondLast = getSecondLastLink();
if (newSecondLast != null) {
newSecondLast.addPropertyChangeListener(secondLastLinkListener);
}
changeSupport
.firePropertyChange(LAST_LINK_PROPERTY, oldLast, chainLink);
changeSupport.firePropertyChange(SIZE_PROPERTY, oldSize, newSize);
}
public void remove(ChainLink link) {
if (chain.isEmpty() || !chain.contains(link)) {
return;
}
ChainLink oldLast = getLastLink();
ChainLink oldSecondLast = getSecondLastLink();
int oldSize = chain.size();
chain.remove(link);
link.removePropertyChangeListener(anyLinkChangeListener);
int newSize = chain.size();
if (newSize == 0) {
chain.add(new ChainLink(null, null));
}
ChainLink newLast = getLastLink();
ChainLink newSecondLast = getSecondLastLink();
if (oldLast != newLast) {
if (oldLast != null) {
oldLast.removePropertyChangeListener(lastLinkListener);
}
if (newLast != null) {
newLast.addPropertyChangeListener(lastLinkListener);
}
}
if (oldSecondLast != newSecondLast) {
if (oldSecondLast != null) {
oldSecondLast
.removePropertyChangeListener(secondLastLinkListener);
}
if (newSecondLast != null) {
newSecondLast.addPropertyChangeListener(secondLastLinkListener);
}
}
changeSupport.firePropertyChange(LAST_LINK_PROPERTY, oldLast, newLast);
changeSupport.firePropertyChange(SIZE_PROPERTY, oldSize, newSize);
}
public void clear() {
if (chain.isEmpty()) {
return;
}
ChainLink oldLast = getLastLink();
ChainLink oldSecondLast = getSecondLastLink();
if (oldLast != null) {
oldLast.removePropertyChangeListener(lastLinkListener);
}
if (oldSecondLast != null) {
oldSecondLast.removePropertyChangeListener(secondLastLinkListener);
}
int oldSize = chain.size();
for (ChainLink link : chain) {
link.removePropertyChangeListener(anyLinkChangeListener);
}
chain.clear();
chain.add(new ChainLink(null, null));
getLastLink().addPropertyChangeListener(anyLinkChangeListener);
int newSize = chain.size();
getLastLink().addPropertyChangeListener(lastLinkListener);
// we know there is no new second last link at this point
changeSupport.firePropertyChange(LAST_LINK_PROPERTY, oldLast,
getLastLink());
changeSupport.firePropertyChange(SIZE_PROPERTY, oldSize, newSize);
}
public boolean canRemove(ChainLink link) {
return canClear() && isLastLink(link);
}
public boolean canChange(ChainLink link) {
return isLastLink(link)
|| (isSecondLastLink(link) && !getLastLink().hasInstance() && !getLastLink()
.hasType());
}
public boolean canClear() {
if (chain.isEmpty()) {
return false;
}
return chain.size() > 1 || (getLastLink().hasInstance() || getLastLink().hasType());
}
public boolean canAddAnyAny() {
return chain.size() > 1 || (getLastLink().hasInstance() || getLastLink().hasType());
}
public List<ChainLink> getLinks() {
return Collections.unmodifiableList(chain);
}
public synchronized List<ChainLink> getLinksSnapshot() {
List<ChainLink> result = new ArrayList<ChainLink>(chain.size());
for (ChainLink chainLink : chain) {
try {
result.add((ChainLink) chainLink.clone());
} catch (CloneNotSupportedException e) {
// we know that ChainLink is Cloneable, so we should never get here
Logger logger = Logger.getLogger(Chain.class);
logger.warn("got a clone not supported exception even though we know that ChainLink is Cloneable", e);
}
}
return result;
}
@Override
public String toString() {
return String.format("Chain: [%s]", chain);
}
/* listener support */
public void addPropertyChangeListener(PropertyChangeListener l) {
changeSupport.addPropertyChangeListener(l);
}
public void addPropertyChangeListener(String propertyName,
PropertyChangeListener l) {
changeSupport.addPropertyChangeListener(propertyName, l);
}
public void removePropertyChangeListener(PropertyChangeListener l) {
changeSupport.removePropertyChangeListener(l);
}
public void removePropertyChangeListener(String propertyName,
PropertyChangeListener l) {
changeSupport.removePropertyChangeListener(propertyName, l);
}
}