/** * Copyright 2007 the dCache team */ package org.dcache.services.info.base; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * The StateUpdate is a simple collection containing zero or more proposed concurrent * changes to dCache's State. Changes are zero or more purges and zero or more * updated metric values. The purges are processed first followed by any state updates. * <p> * A purge clears a branch and all StateComponents below that branch. Subsequent * state updates within the subtree will fill the subtree as expected. * <p> * Each state update change consists of a StatePath and a StateComponent. The StatePath * indicates where the update is to take place and the StateComponent is the new value * to be stored. * <p> * StateUpdate objects are immutable, providing read-only access to proposed set of changes. * * @see StateTransition */ public class StateUpdate { private static final Logger LOGGER = LoggerFactory.getLogger(StateUpdate.class); /** * A single update to a dCache metric. */ private static class StateUpdateInstance { final StatePath _path; final StateComponent _newValue; StateUpdateInstance(StatePath path, StateComponent newValue) { _path = path; _newValue = newValue; } } /** StateComponents to create or update */ private final List<StateUpdateInstance> _updates = new ArrayList<>(); /** A list of subtrees to purge */ private final List<StatePath> _purge = new ArrayList<>(); /** * Discover whether a StateUpdate object intends to update the specified location in * the state tree with a metric that is equal to the given StateComponent. * <p> * This method is intended for unit-testing only: normal operation should not need to * call this method. * @param path the StatePath of the item in question. * @param value the StateComponent that is being queried. */ public boolean hasUpdate(StatePath path, StateComponent value) { if (path == null || value == null) { return false; } /** * Yes, this is an ugly O(n) linear search, but should only be for * a small number of entries. */ for (StateUpdateInstance sui : _updates) { if (sui._path.equals(path) && sui._newValue.equals(value)) { return true; } } return false; } /** * Discover whether a StateUpdate object intends to purge the specified location * in the state tree and all child elements. * <p> * This method is intended for unit-testing only: normal operation should not need to * call this method. * @param path the StatePath of the top-most element in the purge subtree * @return true if the StateUpdate intends to purge exactly the stated subtree, false otherwise. */ public boolean hasPurge(StatePath path) { return _purge.contains(path); } /** * Count the number of metrics that are to be updated. * @return the number of metric updates contained within this StateUpdate. */ public int count() { return _updates.size(); } /** * Count the number of StatePaths that are the root of some * purge operation. * @return */ public int countPurges() { return _purge.size(); } /** * Provide a mechanism whereby we can append additional updates to * this state. * @param path: the StatePath of the new StateValue. * @param value: the new */ public void appendUpdate(StatePath path, StateComponent value) { _updates.add(new StateUpdateInstance(path, value)); } /** * Add a Collection of items as a series of StateComposites. These * may be immortal or ephemeral. * @param path The common StatePath for this Collection * @param items The items to add * @param isImmortal true for immortal StateComposites; false for ephemeral */ public void appendUpdateCollection(StatePath path, Collection<String> items, boolean isImmortal) { for (String item : items) { appendUpdate(path.newChild(item), new StateComposite(isImmortal)); } } /** * Add a Collection of items as mortal StateComposite objects. * @param path The common StatePath for this Collection * @param items The Collection of names. * @param lifetime the lifetime, in seconds, for the StateComposites. */ public void appendUpdateCollection(StatePath path, Collection<String> items, long lifetime) { for (String item : items) { appendUpdate(path.newChild(item), new StateComposite(lifetime)); } } /** * Add the name of a path that all elements underneath should be purged from * the information. These purges, if any, happen before any addition or * updating elements. * @param path */ public void purgeUnder(StatePath path) { _purge.add(path); } /** * Go through each of the proposed updates recorded and update the StateTransition object. * @param top the top-most StateComposite within dCache state * @param transition the StateTransition to update. * @throws BadStatePathException */ protected void updateTransition(StateComposite top, StateTransition transition) throws BadStatePathException { BadStatePathException caughtThis = null; LOGGER.trace("preparing transition with {} purge and {} update", _purge.size(), _updates.size()); for (StatePath path : _purge) { top.buildPurgeTransition(transition, null, path); } for (StateUpdateInstance update : _updates) { LOGGER.trace("preparing transition to alter {}", update._path); try { top.buildTransition(null, update._path, update._newValue, transition); } catch (BadStatePathException e) { if (caughtThis == null) { caughtThis = e; } } } if (caughtThis != null) { throw caughtThis; } } /** * A handy routine for extracting information. * @return a human-readable string describing the StateUpdate object */ public String debugInfo() { StringBuilder sb = new StringBuilder(); sb.append("=== StateUpdate ===\n"); sb.append(" Number of purges: ").append(_purge.size()) .append("\n"); for (StatePath purgePath : _purge) { sb.append(" ").append(purgePath).append("\n"); } sb.append(" Number of StateUpdates: ").append(_updates.size()) .append("\n"); for (StateUpdateInstance sui : _updates) { sb.append(" ").append(sui._path).append(" ") .append(sui._newValue).append("\n"); } return sb.toString(); } public void updateComplete() { // This is a no-op method called after an update has been processed. } }