package de.saring.util.data; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; /** * This list contains unique instances of IdObject subclasses. It will never * contain multiple instances with the same ID. It also provides usefull * methods for getting and removing instances by ID or by their index and for * getting new unique IDs. * Its possible to register IdObjectListChangeListener which will be informed * each time the list content has changed. * * @param <T> the object type to store in this list, must be a subclass of IdObject * @author Stefan Saring * @version 1.0 */ public class IdObjectList<T extends IdObject> implements Iterable<T> { /** * Generic list of subclasses of IdObject. */ private final List<T> lIdObjects = new ArrayList<>(); /** * List of listeners which will be notified on each list content change. */ private final List<IdObjectListChangeListener> listChangelisteners = new ArrayList<>(); /** * Returns the IdObject with the specified ID. * * @param id ID of IdObject * @return the IdObject object or null */ public T getByID(int id) { Optional<T> oIdObject = stream().filter(o -> o.getId() == id).findFirst(); return oIdObject.orElse(null); } /** * Returns the IdObject at the specified index. Throws an * IndexOutOfBoundsException when the index is not valid. * * @param index the index of the IdObject * @return the IdObject */ public T getAt(int index) { return lIdObjects.get(index); } /** * Returns the index of the specified object in the list or -1 if it is not * contained. * * @param t the object to lookup in the list * @return the index of the object or -1 */ public int indexOf(T t) { return lIdObjects.indexOf(t); } /** * Checks whether the specified object is contained in list. * * @param t the object to lookup in the list * @return true if the list contains the specified object */ public boolean contains(T t) { return lIdObjects.contains(t); } /** * Stores the specified IdObject in the list. If there is already an * IDObject with that ID then the old object will be overwritten. Otherwise * the new one will be added to the end of the list. * * @param t the IdObject to store (must not be null) */ public void set(T t) { validateEntry(t); try { int index = lIdObjects.indexOf(t); if (index >= 0) { // replace old IdObject if there is one with the ID of the new one this.lIdObjects.set(index, t); } else { // the object has a new ID => add to end of list this.lIdObjects.add(t); } } finally { notifyAllListChangelisteners(t); } } /** * Clears this IdObjectList and adds all IdObjects of the passed list. * Finally all registered ChangeListeners will be notified. * * @param entries list of IdObjects to store (must not be null, entries must not be null and all * entries and must have a valid ID) */ public void clearAndAddAll(final List<T> entries) { Objects.requireNonNull(entries, "List of IdObjects must not be null!"); entries.forEach(entry -> validateEntry(entry)); lIdObjects.clear(); lIdObjects.addAll(entries); notifyAllListChangelisteners(null); } /** * Removes the IdObject with the specified ID from the list. * * @param id ID of IDObject to remove * @return true on success */ public boolean removeByID(int id) { T t = getByID(id); if (t != null) { boolean removed = this.lIdObjects.remove(t); if (removed) { notifyAllListChangelisteners(null); } return removed; } return false; } /** * This method returns an unique ID, which is not in use yet. * * @return a new unused ID */ public int getNewID() { Set<? super Integer> hsIDs = stream() .map(T::getId) .collect(Collectors.toSet()); // find first unused ID int newID = 1; while (hsIDs.contains(newID)) { newID++; } return newID; } /** * Returns the size of the list. * * @return the size of the list */ public int size() { return lIdObjects.size(); } /** * Returns an interator over the list elements in proper sequence. * * @return interator over the list elements */ @Override public Iterator<T> iterator() { return lIdObjects.iterator(); } /** * Returns the Stream of the internal IDObject list for functional processing. * * @return the Stream of the internal IDObject list */ public Stream<T> stream() { return lIdObjects.stream(); } /** * Adds the specified IdObjectListChangeListener to the list of listeners * which will be notified on each list change. * * @param listener the IdObjectListChangeListener to add */ public void addListChangeListener(IdObjectListChangeListener listener) { listChangelisteners.add(listener); } /** * Returns a string representation of this object. * * @return string with object content */ @Override public String toString() { StringBuilder sBuilder = new StringBuilder(); sBuilder.append(this.getClass().getName()).append(":\n"); lIdObjects.forEach(sBuilder::append); return sBuilder.toString(); } /** * Notifies all registered listeners that the content of the list has been * changed. * * @param changedObject the added / changed object (or null when removed or all objects changed) */ protected void notifyAllListChangelisteners(IdObject changedObject) { listChangelisteners.forEach(listener -> listener.listChanged(changedObject)); } /** * Returns the internal list of IdObject. Only subclasses can directly * access this list. * * @return the internal list of IdObject */ protected List<T> getIDObjects() { return lIdObjects; } /** * Validates the IdDateObject to be stored in this list. RuntimeExceptions will be thrown on errors. * * @param t entry to validate */ protected void validateEntry(final T t) { Objects.requireNonNull(t, "IdObject must not be null!"); if (t.getId() <= 0) { throw new IllegalArgumentException("ID must be a positive integer > 0!"); } } }