/* * 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 2 of the License, or (at your option) any later * version. You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.aitools.programd.predicates; import java.util.Map; import org.aitools.programd.Bots; import org.aitools.programd.Core; import org.aitools.programd.CoreSettings; import org.aitools.util.xml.Characters; import org.apache.log4j.Logger; /** * <p> * Maintains predicate values for userids. Every public set and get method checks the size of the cache, and saves out * part of it if it has exceeded a configurable limit. * </p> * <p> * This currently has the defect that it doesn't choose intelligently which userids' predicates to cache (it should do * this for the ones who have not been heard from the longest). The HashMap that contains the predicates (keyed by * userid) makes no guarantees about order. :-( * </p> * * @author <a href="mailto:noel@aitools.org">Noel Bush</a> */ abstract public class PredicateManager { /** Maximum index of indexed predicates. */ public static final int MAX_INDEX = 5; /** * Returns, from the cache, an ArrayList of values assigned to a <code>name</code> for a predicate for a * <code>userid</code>. If the <code>name</code> exists in a predicate for the <code>userid</code> but it is not * indexed, it is converted into an indexed value. If it does not exist at all, a * <code>NoSuchPredicateException</code> is thrown. * * @param name the name of the predicate * @param predicates the existing map of predicates * @return a list of values assigned to a <code>name</code> for a predicate for a <code>userid</code> * @throws NoSuchPredicateException if no values are assigned to the <code>name</code> */ protected static PredicateValue getMultivaluedPredicateValue(String name, PredicateMap predicates) throws NoSuchPredicateException { if (predicates.size() > 0 && predicates.containsKey(name)) { return predicates.get(name).becomeMultiValued(); } // If the predicate is not found, throw an exception. throw new NoSuchPredicateException(name); } /** The number of predicate set operations to allow before saving predicates. */ private int _flushSize; /** A counter for tracking the number of predicate set operations. */ protected int _setCount = 0; /** The predicate empty default. */ protected String _predicateEmptyDefault; /** The Core that owns this. */ protected Core _core; /** The Bots object in use. */ protected Bots _bots; /** The general Program D logger. */ protected Logger _logger; /** * Creates a new PredicateMaster with the given Core as its owner. * * @param core the Core that owns this PredicateMaster */ public PredicateManager(Core core) { this._core = core; this._bots = this._core.getBots(); CoreSettings coreSettings = this._core.getSettings(); this._predicateEmptyDefault = coreSettings.getPredicateEmptyDefault(); this._logger = Logger.getLogger("programd"); this._flushSize = coreSettings.getPredicateFlushPeriod(); this.initialize(); } /** * Returns the best available default predicate <code>value</code> for a predicate <code>name</code> * * @param name the predicate name * @param botid * @return the best available default predicate */ protected String bestAvailableDefault(String name, String botid) { Map<String, PredicateInfo> predicatesInfo = this._bots.get(botid).getPredicatesInfo(); // There may be an individual default defined. if (predicatesInfo.containsKey(name)) { return predicatesInfo.get(name).getDefaultValue(); } // If not, return the global empty default. if (this._logger.isDebugEnabled()) { this._logger.debug(String.format("No default value available for \"%s\"; returning predicate empty default.", name)); } return this._predicateEmptyDefault; } /** * Saves all predicates and removes them from memory. */ abstract protected void dumpPredicates(); /** * Checks the predicate cache, and saves out predicates if necessary. */ @SuppressWarnings("boxing") protected void flushIfNecessary() { // See if we have exceeded the cacheMax. if (this._setCount > this._flushSize) { if (this._logger.isDebugEnabled()) { this._logger.debug(String.format("Set count %d exceeds flush size %d.", this._setCount, this._flushSize)); } this.saveAll(); } } /** * Gets the predicate <code>value</code> associated with a <code>name</code> at a given <code>index</code> for a given * <code>userid</code>. * * @param name the predicate name * @param index the index * @param userid the userid * @param botid * @return the <code>value</code> associated with the given <code>name</code> at the given <code>index</code>, for the * given <code>userid</code> */ @SuppressWarnings("boxing") public synchronized String get(String name, int index, String userid, String botid) { // Get existing or new predicates map for userid. PredicateMap predicates = this._bots.get(botid).predicatesFor(userid); String result = null; // Get the list of values. PredicateValue value = null; if (!predicates.containsKey(name)) { // No values cached; try loading. if (this._logger.isDebugEnabled()) { this._logger.debug(String.format("Predicate \"%s\" is not cached; attempting to load.", name)); } try { value = this.loadMultivaluedPredicate(name, predicates, userid, botid); if (this._logger.isDebugEnabled()) { this._logger.debug(String.format("Successfully loaded predicate \"%s\".", name)); } predicates.put(name, value); } catch (NoSuchPredicateException e) { // Still no list, so set and cache default. if (this._logger.isDebugEnabled()) { this._logger .debug(String.format("Could not load predicate \"%s\"; setting to best available default.", name)); } result = this.bestAvailableDefault(name, botid); predicates.put(name, result); } } else { try { value = getMultivaluedPredicateValue(name, predicates); if (this._logger.isDebugEnabled()) { this._logger.debug(String.format("Successfully retrieved multi-valued predicate \"%s\" from cache.", name)); } } catch (NoSuchPredicateException e) { assert false : "predicates.containsKey(name) but getMultivaluedPredicateValue(name, predicates) throws NoSuchPredicateException!"; } } if (value != null) { // The index may be invalid. try { // Get the value at index. result = value.get(index); } catch (IndexOutOfBoundsException e) { try { value = this.loadMultivaluedPredicate(name, predicates, userid, botid); if (this._logger.isDebugEnabled()) { this._logger.debug(String.format("Successfully loaded predicate \"%s\".", name)); } predicates.put(name, value); } catch (NoSuchPredicateException ee) { assert false; } try { // Get the value at index. result = value.get(index); } catch (IndexOutOfBoundsException ee) { // Return the best available default. result = this.bestAvailableDefault(name, botid); this._logger .warn(String .format( "Index %d not available for predicate \"%s\" (user \"%s\", bot \"%s\"). Returning best available default.", index, name, userid, botid)); } } } // Return the value. return result; } /** * Gets the predicate <code>value</code> associated with a <code>name</code> for a given <code>userid</code>. * * @param name the predicate name * @param userid the userid * @param botid * @return the <code>value</code> associated with the given <code>name</code>, for the given <code>userid</code> */ public synchronized String get(String name, String userid, String botid) { // Get existing or new predicates map for userid. PredicateMap predicates = this._bots.get(botid).predicatesFor(userid); // Try to get the predicate value from the cache. if (predicates.containsKey(name)) { return predicates.get(name).getFirstValue(); } // otherwise... if (this._logger.isDebugEnabled()) { this._logger.debug(String.format("Predicate \"%s\" is not cached.", name)); } String loadedValue; try { loadedValue = this.loadPredicate(name, userid, botid); if (this._logger.isDebugEnabled()) { this._logger.debug(String.format("Successfully loaded predicate \"%s\".", name)); } } catch (NoSuchPredicateException e) { // If not found, set and cache the best available default. if (this._logger.isDebugEnabled()) { this._logger.debug(String.format("Could not load predicate \"%s\"; setting to best available default.", name)); } loadedValue = this.bestAvailableDefault(name, botid); } // Cache it. predicates.put(name, new PredicateValue(loadedValue)); // Return the loaded value. return loadedValue; } /** * Returns a value list one way or another: first tries to get it from the cache, then tries to load it from the * ActiveMultiplexor; finally creates a new one if the preceding failed. * * @param name the predicate <code>name</code> * @param predicates the user predicates map * @param userid the userid for which to return the value list * @param botid the botid for which to return the value list * @return a multi-valued <code>PredicateValue</code> from <code>userPredicates</code> for <code>name</code> for * <code>userid</code> */ protected PredicateValue getLoadOrCreateMultivaluedPredicate(String name, PredicateMap predicates, String userid, String botid) { PredicateValue value = null; if (!predicates.containsKey(name)) { // No list found in cache; try load. if (this._logger.isDebugEnabled()) { this._logger.debug(String.format("Predicate \"%s\" not cached; attempting to load.", name)); } try { value = this.loadMultivaluedPredicate(name, predicates, userid, botid); if (this._logger.isDebugEnabled()) { this._logger.debug(String.format("Successfully loaded predicate \"%s\".", name)); } predicates.put(name, value); } catch (NoSuchPredicateException ee) { // Still no list, so create new one. if (this._logger.isDebugEnabled()) { this._logger .debug(String.format("Could not load predicate \"%s\"; setting to best available default.", name)); } value = new PredicateValue(this._predicateEmptyDefault).becomeMultiValued(); predicates.put(name, value); } } else { try { value = getMultivaluedPredicateValue(name, predicates); } catch (NoSuchPredicateException e) { assert false : "predicates.containsKey(name) but getMultivaluedPredicateValue(name, predicates) throws NoSuchPredicateException!"; } } return value; } /** * Does whatever initialization is needed for the particular PredicateManager. */ abstract public void initialize(); /** * Tries to load a predicate with <code>name</code> for <code>userid</code> from the Multiplexor into the * <code>predicates</code>. If successful, tries to get the value list for name. If unsuccessful, throws a * NoSuchPredicateException. * * @param name the predicate <code>name</code> * @param predicates the user predicates (must not be null!) * @param userid the userid * @param botid * @return an ArrayList of values assigned to a <code>name</code> for a predicate for a <code>userid</code> * @throws NoSuchPredicateException if no values are assigned to the <code>name</code> * @throws NullPointerException if <code>userPredicates</code> is null */ @SuppressWarnings("boxing") protected PredicateValue loadMultivaluedPredicate(String name, PredicateMap predicates, String userid, String botid) throws NoSuchPredicateException, NullPointerException { // Prevent this from being called with a null predicates map. if (predicates == null) { throw new NullPointerException("Cannot call loadMultivaluedPredicate with null predicates!"); } // Try to load the predicate as an indexed predicate. int index = 1; String loadedValue; try { loadedValue = this.loadPredicate(String.format("%s.%d", name, index), userid, botid); } catch (NoSuchPredicateException e) { throw new NoSuchPredicateException(name); } // If this succeeded, get/create the new values list in the predicates. PredicateValue value = predicates.get(name); if (value == null) { value = new PredicateValue(loadedValue); predicates.put(name, value); } else { value.add(1, loadedValue); } // Now load as many more as possible up to MAX_INDEX. for (index = 2; index <= MAX_INDEX; index++) { try { value.add(index, this.loadPredicate(String.format("%s.%d", name, index), userid, botid)); } catch (NoSuchPredicateException e) { if (this._logger.isDebugEnabled()) { this._logger.debug(String.format("Exceeded maximum index for \"%s\" with %d.", name, index)); } break; } } return value; } /** * Loads a predicate for a given user/bot combination. * * @param name predicate name * @param user user identifier * @param bot bot identifier * @return the predicate value * @throws NoSuchPredicateException if there is no predicate with this name */ abstract protected String loadPredicate(String name, String user, String bot) throws NoSuchPredicateException; /** * Returns the name or value of a predicate, depending on whether or not it is "return-name-when-set". * * @param name the predicate name * @param value the predicate value * @param botid * @return the appropriate result (name or value depending on predicate settings) */ protected String nameOrValue(String name, String value, String botid) { Map<String, PredicateInfo> predicatesInfo = this._bots.get(botid).getPredicatesInfo(); // Check if any info is known about this predicate. if (predicatesInfo.containsKey(name)) { // If so, find out whether to return its name or the value. if (predicatesInfo.get(name).returnNameWhenSet()) { return name; } } return value; } /** * Pushes a new <code>value</code> onto an indexed predicate <code>name</code> for a given <code>userid</code>, and * returns either the <code>name</code> or the <code>value</code>, depending on the predicate type. * * @param name the predicate name * @param newValue the new predicate value * @param userid the userid * @param botid * @return the <code>name</code> or the <code>value</code>, depending on the predicate type */ public synchronized String push(String name, String newValue, String userid, String botid) { // Get existing or new predicates map for userid. PredicateMap userPredicates = this._bots.get(botid).predicatesFor(userid); // Get, load or create the list of values. PredicateValue value = this.getLoadOrCreateMultivaluedPredicate(name, userPredicates, userid, botid); // Push the new value onto the indexed predicate list. value.push(Characters.removeMarkup(newValue)); // Increment the set count. this._setCount++; // Flush if necessary. this.flushIfNecessary(); // Return the name or value. return this.nameOrValue(name, newValue, botid); } /** * Saves all predicates and empties the caches. */ public void saveAll() { if (this._logger.isDebugEnabled()) { this._logger.debug("Saving all predicates."); } this.dumpPredicates(); this._setCount = 0; } /** * Sets a <code>value</code> of an indexed predicate <code>name</code> for a given <code>userid</code>, and returns * either the <code>name</code> or the <code>value</code>, depending on the predicate type. * * @param name the predicate name * @param index the index at which to set the value * @param valueToSet the predicate value to set * @param userid the userid * @param botid * @return the <code>name</code> or the <code>value</code>, depending on the predicate type */ public synchronized String set(String name, int index, String valueToSet, String userid, String botid) { // Get existing or new predicates map for userid. PredicateMap predicates = this._bots.get(botid).predicatesFor(userid); // Get, load or create the list of values. PredicateValue value = this.getLoadOrCreateMultivaluedPredicate(name, predicates, userid, botid); // Try to set the predicate value at the index. value.add(index, valueToSet); // Increment the set count. this._setCount++; // Flush if necessary. this.flushIfNecessary(); // Return the name or value. return this.nameOrValue(name, valueToSet, botid); } /** * Sets a predicate <code>value</code> against a predicate <code>name</code> for a given userid, and returns either * the <code>name</code> or the <code>value</code>, depending on the predicate type. * * @param name the predicate name * @param value the predicate value * @param userid the userid * @param botid * @return the <code>name</code> or the <code>value</code>, depending on the predicate type */ public synchronized String set(String name, String value, String userid, String botid) { // Get existing or new predicates map for userid. PredicateMap predicates = this._bots.get(botid).predicatesFor(userid); // Put the new value into the predicate. predicates.put(name, new PredicateValue(value)); // Increment the set count. this._setCount++; // Flush if necessary. this.flushIfNecessary(); // Return the name or value. return this.nameOrValue(name, value, botid); } }