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* 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); } } /** * Escape the given value to VCF standard. * * @param value * the value to escape * * @return the escaped value */ protected String escape(String value) { if (value == null) return null; return value.replaceAll(",", "\\\\,").replaceAll(";", "\\\\;") .replaceAll("\n", "\\\\n"); } /** * Escape the given value to VCF standard. * * @param value * the value to escape * * @return the escaped value */ protected String unescape(String value) { if (value == null) return null; return value.replaceAll("\\\\,", ",").replaceAll("\\\\;", ";") .replaceAll("\\\\n", "\n"); } /** * 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