/*
* Copyright (C) 2012 Jason Gedge <http://www.gedge.ca>
*
* This file is part of the OpGraph project.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ca.gedge.opgraph.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import ca.gedge.opgraph.util.Pair;
/**
* A breadcrumb maintains a linear navigation history.
*
* @param <S> the type of state
* @param <V> the type of value associated with a state
*/
public class Breadcrumb<S, V> implements Iterable<Pair<S, V>> {
/** The state stack */
private LinkedList<S> states = new LinkedList<S>();
/** The values stack */
private LinkedList<V> values = new LinkedList<V>();
/**
* Gets whether or not the breadcrumb contains a state.
*
* @param state the state to look for
*
* @return <code>true</code> if the breadcrumb contains the given state,
* <code>false</code> otherwise
*/
public boolean containsState(S state) {
return states.contains(state);
}
/**
* Gets whether or not this breadcrumb is empty.
*
* @return <code>true</code> if this breadcrumb is empty,
* <code>false</code> otherwise
*/
public boolean isEmpty() {
return states.isEmpty();
}
/**
* Gets the current state of this breadcrumb.
*
* @return the current state, or <code>null</code> if no states available
*/
public S getCurrentState() {
return (isEmpty() ? null : states.getFirst());
}
/**
* Gets the value associated with the current state of this breadcrumb.
*
* @return the current state's value, or <code>null</code> if no states available
*/
public V getCurrentValue() {
return (isEmpty() ? null : values.getFirst());
}
/**
* Gets the number of states in this breadcrumb.
*
* @return the number of states
*/
public int size() {
return states.size();
}
/**
* Empties this breadcrumb.
*/
public void clear() {
if(!isEmpty()) {
final S oldState = getCurrentState();
states.clear();
values.clear();
fireStateChanged(oldState, null);
}
}
/**
* Gets the state at the specified index from the current state.
*
* @param index the index of the state to peek at
*
* @return the state at the given index (from the current state)
*
* @throws IndexOutOfBoundsException if the index is out of range (i.e.,
* <code>index < 0 || index > size()</code>
*/
public S peekState(int index) {
return states.get(index);
}
/**
* Empties this breadcrumb.
*/
public void popState() {
if(!isEmpty()) {
final S oldState = getCurrentState();
states.removeFirst();
values.removeFirst();
final S newState = getCurrentState();
fireStateChanged(oldState, newState);
}
}
/**
* Goes to the given state. If the state is part of this breadcrumb, all
* states following it will be removed.
*
* @param state the state to go to
*/
public void gotoState(S state) {
int index = 0;
for(; index < states.size(); ++index) {
if(states.get(index) == state)
break;
}
if(index > 0 && index < states.size()) {
final S oldState = getCurrentState();
while(states.getFirst() != state) {
states.removeFirst();
values.removeFirst();
}
fireStateChanged(oldState, state);
}
}
/**
* Sets the complete state of the breadcrumb to a given list of
* state/value pairs.
*
* @param states the new set of states
*/
public void set(List<Pair<S, V>> states) {
final S oldState = getCurrentState();
// Clear and add in new
this.states.clear();
this.values.clear();
for(Pair<S, V> state : states) {
this.states.addFirst(state.getFirst());
this.values.addFirst(state.getSecond());
fireStateAdded(state.getFirst(), state.getSecond());
}
fireStateChanged(oldState, getCurrentState());
}
/**
* Append the given state to this breadcrumb with a <code>null</code> value.
*
* @param state the state
*/
public void addState(final S state) {
addState(state, null);
}
/**
* Append the given state/value pair to this breadcrumb.
*
* @param state the state
* @param value the value to associate with the given state
*/
public void addState(S state, V value) {
final S oldState = getCurrentState();
states.addFirst(state);
values.addFirst(value);
fireStateAdded(state, value);
fireStateChanged(oldState, state);
}
/**
* Gets the set of states as an immutable list.
*
* @return the list of states
*/
public List<S> getStates() {
return Collections.unmodifiableList(states);
}
/**
* Gets the set of values as an immutable list.
*
* @return the list of values
*/
public List<V> getValues() {
return Collections.unmodifiableList(values);
}
//
// Iterable
//
@Override
public Iterator<Pair<S, V>> iterator() {
final Iterator<S> iterS = states.iterator();
final Iterator<V> iterV = values.iterator();
return new Iterator<Pair<S, V>>() {
@Override
public boolean hasNext() {
return iterS.hasNext();
}
@Override
public Pair<S, V> next() {
return new Pair<S, V>(iterS.next(), iterV.next());
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove() not supported in Breadcrumb");
}
};
}
//
// Listeners
//
private ArrayList<BreadcrumbListener<S, V>> listeners = new ArrayList<BreadcrumbListener<S, V>>();
/**
* Adds a breadcrumb listener to this breadcrumb.
*
* @param listener the listener to add
*/
public void addBreadcrumbListener(BreadcrumbListener<S, V> listener) {
synchronized(listeners) {
if(listener != null && !listeners.contains(listener))
listeners.add(listener);
}
}
/**
* Removes a breadcrumb listener from this breadcrumb.
*
* @param listener the listener to remove
*/
public void removeBreadcrumbListener(BreadcrumbListener<S, V> listener) {
synchronized(listeners) {
listeners.remove(listener);
}
}
protected void fireStateChanged(S oldState, S newState) {
synchronized(listeners) {
for(BreadcrumbListener<S, V> listener : listeners)
listener.stateChanged(oldState, newState);
}
}
protected void fireStateAdded(S state, V value) {
synchronized(listeners) {
for(BreadcrumbListener<S, V> listener : listeners)
listener.stateAdded(state, value);
}
}
}