package be.nikiroo.jvcard;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import be.nikiroo.jvcard.resources.StringUtils;
/**
* This class is basically a List with a parent and a "dirty" state check. It
* sends all commands down to the initial list, but will mark itself and its
* children as dirty or not when needed.
*
*
* All child elements can identify their parent, and must not be added to 2
* different objects without without first being removed from the previous one.
*
*
*
* The dirty state is bubbling up (when dirty = true) or down (when dirty =
* false) -- so, making changes to a child element will also mark its parent as
* "dirty", and marking an element as pristine will also affect all its child
* elements.
*
*
* @author niki
*
* @param
* the type of the child elements
*/
public abstract class BaseClass> implements List {
protected boolean dirty;
protected BaseClass> parent;
private List list;
private Comparator comparator = new Comparator() {
@Override
public int compare(E o1, E o2) {
if (o1 == null && o2 == null)
return 0;
if (o1 == null && o2 != null)
return -1;
if (o1 != null && o2 == null)
return 1;
return o1.getId().compareTo(o2.getId());
}
};
/**
* Create a new {@link BaseClass} with the items in the given list as its
* descendants.
*
* Note: the elements will be copied from the {@link List}, you cannot
* manage the {@link List} from outside
*
* @param list
* the descendants of this object, or NULL if none
*/
protected BaseClass(List list) {
this.list = new ArrayList();
if (list != null) {
this.list.addAll(list);
}
for (E child : this) {
_enter(child, true);
}
}
/**
* Check if this element has unsaved changes.
*
* @return TRUE if it has
*/
public boolean isDirty() {
return dirty;
}
/**
* Delete this element from its parent if any.
*
* @return TRUE in case of success
*/
public boolean delete() {
if (parent != null) {
return parent.remove(this);
}
return false;
}
/**
* Replace the elements contained in this with those in the given
* {@link List}.
*
* Note: the elements will be copied from the {@link List}, you cannot
* manage the {@link List} from outside
*
* @param list
* the list of new elements
*/
public void replaceListContent(List list) {
List del = new LinkedList();
List add = new LinkedList();
if (!compare(list, add, del, del, add)) {
removeAll(del);
addAll(add);
}
}
/**
* Compare the elements contained in this with those in the given
* {@link List}. It will return TRUE in case of equality, will return FALSE
* if not.
*
* If not equals, the differences will be represented by the given
* {@link List}s if they are not NULL.
*
*
added will represent the elements in list but not
* in this
*
removed will represent the elements in this but not
* in list
*
from will represent the elements in list that are
* already contained in this but are not equals to them (the
* original element from this is stored here)
*
to will represent the elements in list that are
* already contained in this but are not equals to them (the
* changed element from list is stored here)
*
*
* @param list
* the list of new elements
* @param added
* the list to add the added elements to, or NULL
* @param removed
* the list to add the removed elements to, or NULL
* @param from
* the map to add the from elements, or NULL
* @param to
* the map to add the to elements, or NULL
*
* @return TRUE if the elements are identical
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public boolean compare(List list, List added, List removed,
List from, List to) {
Collections.sort(this.list, comparator);
List mine = new LinkedList(this.list);
List other = new LinkedList(list);
Collections.sort(other, comparator);
boolean equ = true;
E here = mine.size() > 0 ? mine.remove(0) : null;
E there = other.size() > 0 ? other.remove(0) : null;
while (here != null || there != null) {
if (here == null
|| (there != null && comparator.compare(here, there) > 0)) {
if (added != null)
added.add(there);
there = null;
equ = false;
} else if (there == null || comparator.compare(here, there) < 0) {
if (removed != null)
removed.add(here);
here = null;
equ = false;
} else {
// they represent the same item
if (!((BaseClass) here).isEquals(there, false)) {
if (from != null)
from.add(here);
if (to != null)
to.add(there);
equ = false;
}
here = null;
there = null;
}
if (here == null && mine.size() > 0)
here = mine.remove(0);
if (there == null && other.size() > 0)
there = other.remove(0);
}
return equ;
}
/**
* Check if the given instance and this one represent the same objects (they
* may have different states).
*
* @param other
* the other instance
*
* @return TRUE if they represent the same object
*/
public boolean isSame(BaseClass other) {
if (other == null)
return false;
if (!getClass().equals(other.getClass()))
return false;
return getId().equals(other.getId());
}
/**
* Check if the given instance and this one are equivalent (both objects in
* the same state, all child elements equivalent).
*
* @param other
* the other instance
*
* @param contentOnly
* do not check the state of the object itslef, only its content
*
* @return TRUE if they are equivalent
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public boolean isEquals(BaseClass other, boolean contentOnly) {
if (other == null)
return false;
if (size() != other.size())
return false;
if (!contentOnly) {
if (!isSame(other))
return false;
if (!getState().equals(other.getState()))
return false;
}
Collections.sort(list, comparator);
Collections.sort(other.list, other.comparator);
for (int index = 0; index < size(); index++) {
if (!((BaseClass) get(index)).isEquals(other.get(index), false))
return false;
}
return true;
}
/**
* Get the recursive state of the current object, i.e., its children
* included. It represents the full state information about this object's
* children. It may not contains spaces nor new lines.
*
*
* Not that this state is lossy. You cannot retrieve the data from
* the state, it can only be used as an ID to check if data are identical.
*
*
* @param self
* also include state information about the current object itself
* (as opposed to its children)
*
* @return a {@link String} representing the current content state of this
* object, i.e., its children included
*/
public String getContentState(boolean self) {
StringBuilder builder = new StringBuilder();
buildContentStateRaw(builder, self);
return StringUtils.getHash(builder.toString());
}
/**
* Return the (first) child element with the given ID or NULL if not found.
*
* @param id
* the id to look for
*
* @return the child element or NULL
*/
public E getById(String id) {
for (E child : this) {
if (id == null) {
if (child.getId() == null)
return child;
} else {
if (id.equals(child.getId()))
return child;
}
}
return null;
}
/**
* Return a {@link String} that can be used to identify this object in DEBUG
* mode, i.e., a "toString" method that can identify the object's content
* but still be readable in a log.
*
* @param depth
* the depth into which to descend (0 = only this object, not its
* children)
*
* @return the debug {@link String}
*/
public String getDebugInfo(int depth) {
StringBuilder builder = new StringBuilder();
getDebugInfo(builder, depth, 0);
return builder.toString();
}
/**
* Return the current ID of this object -- it is allowed to change over time
* (so, do not cache it).
*
* @return the current ID
*/
abstract public String getId();
/**
* Get the state of the current object, children not included. It
* represents the full state information about this object, but do not check
* its children (see {@link BaseClass#getContentState()} for that). It may
* not contains spaces nor new lines.
*
*
* Not that this state is lossy. You cannot retrieve the data from
* the state, it can only be used as an ID to check if thw data are
* identical.
*
*
* @return a {@link String} representing the current state of this object,
* children not included
*/
abstract public String getState();
/**
* Get the recursive state of the current object, i.e., its children
* included. It represents the full state information about this object's
* children.
*
* It is not hashed.
*
* @param builder
* the {@link StringBuilder} that will represent the current
* content state of this object, i.e., its children included
* @param self
* also include state information about the current object itself
* (as opposed to its children)
*/
void buildContentStateRaw(StringBuilder builder, boolean self) {
Collections.sort(this.list, comparator);
if (self)
builder.append(getState());
for (E child : this) {
child.buildContentStateRaw(builder, true);
}
}
/**
* Populate a {@link StringBuilder} that can be used to identify this object
* in DEBUG mode, i.e., a "toString" method that can identify the object's
* content but still be readable in a log.
*
* @param depth
* the depth into which to descend (0 = only this object, not its
* children)
*
* @param tab
* the current tabulation increment
*/
void getDebugInfo(StringBuilder builder, int depth, int tab) {
for (int i = 0; i < tab; i++)
builder.append(" ");
builder.append(getContentState(false) + " " + getId());
if (depth > 0)
builder.append(": [");
if (depth > 0) {
for (E child : this) {
builder.append("\n");
child.getDebugInfo(builder, depth - 1, tab + 1);
}
}
if (depth > 0) {
builder.append("\n");
for (int i = 0; i < tab; i++)
builder.append(" ");
builder.append("]");
}
}
/**
* Notify that this element has unsaved changes.
*/
void setDirty() {
dirty = true;
if (parent != null) {
parent.setDirty();
}
}
/**
* Notify this element and all its descendants that it is in pristine
* state (as opposed to dirty).
*/
void setPristine() {
dirty = false;
for (E child : this) {
child.setPristine();
}
}
/**
* Set the parent of this element and all its descendants.
*
* @param parent
* the new parent
*/
void setParent(BaseClass> parent) {
this.parent = parent;
for (E child : this) {
child.setParent(this);
}
}
/**
* Each element that leaves the parent will pass trough here.
*
* @param child
* the element to remove from this
*/
private void _leave(E child) {
if (child.parent != null && child.parent != this) {
throw new InvalidParameterException(
"You are removing this child from its rightful parent, it must be yours to do so");
}
child.parent = null;
setDirty();
}
/**
* Each element that enters the parent will pass trough here.
*
* @param child
* the element to add to this
*/
private void _enter(E child) {
_enter(child, false);
}
/**
* Each element that enters the parent will pass trough here.
*
* @param child
* the element to add to this
*/
private void _enter(E child, boolean initialLoad) {
if (child.parent != null && child.parent != this) {
throw new InvalidParameterException(
"You are stealing this child from its rightful parent, you must remove it first");
}
child.setParent(this);
if (!initialLoad) {
setDirty();
child.setDirty();
}
}
@Override
public boolean add(E e) {
_enter(e, false);
return list.add(e);
}
@Override
@SuppressWarnings("unchecked")
public boolean remove(Object o) {
if (list.remove(o)) {
if (o instanceof BaseClass>) {
_leave((E) o); // expected warning
}
return true;
}
return false;
}
@Override
public boolean addAll(Collection extends E> c) {
for (E child : c) {
_enter(child);
}
return list.addAll(c);
}
@Override
public boolean addAll(int index, Collection extends E> c) {
for (E child : c) {
_enter(child);
}
return list.addAll(index, c);
}
@Override
public boolean removeAll(Collection> c) {
boolean changed = false;
for (Object o : c) {
if (remove(o))
changed = true;
}
return changed;
}
@Override
public boolean retainAll(Collection> c) {
ArrayList