/*
* 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.graph;
import java.io.PrintWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import org.aitools.programd.Bot;
import org.aitools.programd.Core;
import org.aitools.programd.util.NoMatchException;
import org.aitools.util.ObjectFactory;
import org.aitools.util.Text;
import org.aitools.util.runtime.DeveloperError;
/**
* This is a memory-based {@link Graphmapper}.
*
* @author <a href="mailto:noel@aitools.org">Noel Bush</a>
*/
public class MemoryGraphmapper extends AbstractGraphmapper {
/** The factory that will be used to create Nodemappers. */
protected ObjectFactory<Nodemapper> NodemapperFactory;
/** A map of loaded file URLs to botids. */
protected Map<URL, Set<String>> _urlCatalog = new HashMap<URL, Set<String>>();
/** A map of KB URLs to <BOTID> nodes. */
protected Map<URL, Set<Nodemapper>> botidNodes = new HashMap<URL, Set<Nodemapper>>();
/** The root {@link Nodemapper}. */
protected Nodemapper root;
/** A count of Nodemappers. */
protected int nodemapperCount = 1;
/**
* Creates a new <code>Graphmaster</code>, reading settings from the given Core.
*
* @param core the CoreSettings object from which to read settings
*/
public MemoryGraphmapper(Core core) {
super(core);
this.NodemapperFactory = new ObjectFactory<Nodemapper>(this._core.getSettings().getNodemapperImplementation());
this.root = this.NodemapperFactory.getNewInstance();
}
/**
* Adds a new path to the <code>Graphmaster</code> at a given node.
*
* @param pathIterator an iterator over the List containing the elements of the path
* @param parent the <code>Nodemapper</code> parent to which the child should be appended
* @param source the source of the original path
* @return <code>Nodemapper</code> which is the result of adding the node
*/
protected Nodemapper add(ListIterator<String> pathIterator, Nodemapper parent, URL source) {
// If there are no more words in the path, return the parent node
if (!pathIterator.hasNext()) {
parent.setTop();
return parent;
}
// Otherwise, get the next word.
String word = pathIterator.next();
Nodemapper nodemapper;
// If the parent contains this word, get the nodemapper with the word.
if (parent.containsKey(word)) {
nodemapper = (Nodemapper) parent.get(word);
}
else {
// Otherwise create a new nodemapper with this word.
nodemapper = this.NodemapperFactory.getNewInstance();
this.nodemapperCount++;
parent.put(word, nodemapper);
nodemapper.setParent(parent);
}
// Associate <BOTID> nodes with their sources.
if (word.equals(BOT)) {
Set<Nodemapper> nodemappers;
if (this.botidNodes.containsKey(source)) {
nodemappers = this.botidNodes.get(source);
}
else {
nodemappers = new HashSet<Nodemapper>();
this.botidNodes.put(source, nodemappers);
}
nodemappers.add(nodemapper);
}
// Return the result of adding the new nodemapper to the parent.
return this.add(pathIterator, nodemapper, source);
}
/**
* @see org.aitools.programd.graph.Graphmapper#addCategory(java.lang.String, java.lang.String, java.lang.String,
* java.lang.String, org.aitools.programd.Bot, java.net.URL)
*/
@Override
public void add(String pattern, String that, String topic, String template, Bot bot, URL source) {
Nodemapper nodemapper = this.add(pattern, that, topic, bot.getID(), source);
String storedTemplate = (String) nodemapper.get(TEMPLATE);
if (storedTemplate == null) {
nodemapper.put(FILENAME, source.toExternalForm());
bot.addToPathMap(source, nodemapper);
nodemapper.put(TEMPLATE, template);
this._totalCategories++;
}
else {
this._duplicateCategories++;
switch (this._mergePolicy) {
case SKIP:
if (this._noteEachMerge) {
this._logger.warn(String.format(
"Skipping path-identical category from \"%s\" which duplicates path of category from \"%s\": %s:%s:%s",
source, nodemapper.get(FILENAME), pattern, that, topic));
}
break;
case OVERWRITE:
if (this._noteEachMerge) {
this._logger.warn(String.format(
"Overwriting path-identical category from \"%s\" with new category from \"%s\". Path: %s:%s:%s",
nodemapper.get(FILENAME), source, pattern, that, topic));
}
nodemapper.put(FILENAME, source);
nodemapper.put(TEMPLATE, template);
break;
case APPEND:
if (this._noteEachMerge) {
this._logger
.warn(String
.format(
"Appending template of category from \"%s\" to template of path-identical category from \"%s\": %s:%s:%s",
source, nodemapper.get(FILENAME), pattern, that, topic));
}
nodemapper.put(FILENAME, String.format("%s, %s", nodemapper.get(FILENAME), source));
nodemapper.put(TEMPLATE, this.appendTemplate(storedTemplate, template));
break;
case COMBINE:
if (this._noteEachMerge) {
this._logger
.warn(String
.format(
"Combining template of category from \"%s\" with template of path-identical category from \"%s\": %s:%s:%s",
source, nodemapper.get(FILENAME), pattern, that, topic));
}
nodemapper.put(FILENAME, String.format("%s, %s", nodemapper.get(FILENAME), source));
String combined = this.combineTemplates(storedTemplate, template);
nodemapper.put(TEMPLATE, combined);
break;
}
}
}
/**
* Adds a new pattern-that-topic path to the <code>Graphmaster</code> root.
*
* @param pattern <pattern/> path component
* @param that <that/> path component
* @param topic <topic/> path component
* @param botid
* @param source the source of this path
* @return <code>Nodemapper</code> which is the result of adding the path.
*/
protected Nodemapper add(String pattern, String that, String topic, String botid, URL source) {
List<String> path = Text.wordSplit(pattern);
path.add(THAT);
path.addAll(Text.wordSplit(that));
path.add(TOPIC);
path.addAll(Text.wordSplit(topic));
path.add(BOT);
path.add(botid);
return this.add(path.listIterator(), this.root, source);
}
/**
* @see org.aitools.programd.graph.AbstractGraphmapper#addForBot(java.net.URL, java.lang.String)
*/
@Override
protected void addForBot(URL path, String botid) {
if (!this._urlCatalog.containsKey(path)) {
throw new IllegalArgumentException("Must not call addForBot() using a URL that has not already been loaded.");
}
if (this._urlCatalog.get(path).contains(botid)) {
throw new IllegalArgumentException(
"Must not call addForBot() using a URL and botid that have already been associated.");
}
if (this._logger.isDebugEnabled()) {
this._logger.debug(String.format("Adding botid \"%s\" to all paths associated with \"%s\".", botid, path));
}
for (Nodemapper nodemapper : this.botidNodes.get(path)) {
// Hook up with the existing template.
Object t = nodemapper.get(nodemapper.keySet().iterator().next());
nodemapper.put(botid, t);
this._totalCategories++;
}
this._urlCatalog.get(path).add(botid);
}
@Override
protected void associateBotIDWithFilename(String botid, URL filename) {
Set<String> botids;
if (this._urlCatalog.containsKey(filename)) {
botids = this._urlCatalog.get(filename);
}
else {
botids = new HashSet<String>();
this._urlCatalog.put(filename, botids);
}
botids.add(botid);
}
@Override
protected boolean isAlreadyLoaded(URL filename) {
return this._urlCatalog.containsKey(filename);
}
@Override
protected boolean isAlreadyLoadedForBot(URL filename, String botid) {
return this.isAlreadyLoaded(filename) && this._urlCatalog.get(filename).contains(botid);
}
/**
* Searches for a match in the <code>Graphmaster</code> to a given path. This is a low-level prototype, used for
* internal recursion.
*
* @see #match(String, String, String, String)
* @param nodemapper the nodemapper where we start matching
* @param parent the parent of the nodemapper where we start matching
* @param input the input path (possibly a sublist of the original)
* @param wildcardContent contents absorbed by a wildcard
* @param path the path matched so far
* @param match an object containing information about the match
* @param matchState state variable tracking which part of the path we're in
* @param expiration when this response process expires
* @return the leaf nodemapper at which the match ends
* @throws NoMatchException if match time expires
*/
@SuppressWarnings("boxing")
protected Nodemapper match(Nodemapper nodemapper, Nodemapper parent, List<String> input, String wildcardContent,
StringBuilder path, Match match, Match.State matchState, long expiration) throws NoMatchException {
if (nodemapper == null) {
return null;
}
// Return null if expiration has been reached.
if (System.currentTimeMillis() >= expiration) {
throw new NoMatchException("Match time expired.");
}
Nodemapper nextNodemapper = null;
// Halt matching if this nodemapper is higher than the length of the input.
if (input.size() < nodemapper.getHeight()) {
if (this._matchLogger.isDebugEnabled()) {
this._matchLogger.debug(String.format(
"Halting match because input size %d < nodemapper height %d.%ninput: %s%nnodemapper: %s", input.size(),
nodemapper.getHeight(), input.toString(), nodemapper.toString()));
}
return null;
}
// If no more tokens in the input, see if this is a template.
if (input.size() == 0) {
// If so, the path
// component is the botid.
if (nodemapper.containsKey(TEMPLATE)) {
match.setBotID(path.toString());
match.setTemplate((String) nodemapper.get(TEMPLATE));
match.setFilenames(Arrays.asList(((String) nodemapper.get(FILENAME)).split(",")));
return nodemapper;
}
// (otherwise...)
return null;
}
// Take the first word of the input as the head.
String head = input.get(0).trim();
// Take the rest as the tail.
List<String> tail = input.subList(1, input.size());
// Now proceed through the AIML matching sequence: _, a-z, *.
Match.State _matchState = matchState;
// See if this nodemapper has a _ wildcard. _ comes first in the AIML "alphabet".
nextNodemapper = this.match(UNDERSCORE, // key
_matchState, // target match state for wildcard content
nodemapper, // current nodemapper
tail, // current tail
true, // append new path? yes
wildcardContent, // current wildcard content
head, // new wildcard content
path, // current path
match, // match object
_matchState, // current match state
expiration // expiration timestamp
);
if (nextNodemapper != null) {
return nextNodemapper;
}
/*
* The nodemapper may have contained a _, but this led to no match. Or it didn't contain a _ at all. So let's see if
* it contains the head.
*/
if (nodemapper.containsKey(head)) {
/*
* Check now whether this head is a marker for the <that>, <topic> or <botid> segments of the path. If it is, save
* the contents of the thereby terminated path component, and then set the new match state variable accordingly.
*/
boolean isMarker = false;
if (head.startsWith("<")) {
match.setPathComponent(_matchState, path.toString().toUpperCase());
if (head.equals(THAT)) {
isMarker = true;
_matchState = Match.State.IN_THAT;
}
else if (head.equals(TOPIC)) {
isMarker = true;
_matchState = Match.State.IN_TOPIC;
}
else if (head.equals(BOT)) {
isMarker = true;
_matchState = Match.State.IN_BOTID;
}
}
nextNodemapper = this.match(head, // key
isMarker ? _matchState.preceding() : null, // target match state for wildcard content
nodemapper, // current nodemapper
tail, // current tail
!isMarker, // append new path? (only if this is not a marker)
wildcardContent, // current wildcard content (empty if this is a marker)
isMarker ? "" : wildcardContent, // new wildcard content
path, // current path
match, // match object
_matchState, // current match state
expiration // expiration timestamp
);
if (nextNodemapper != null) {
return nextNodemapper;
}
}
/*
* The nodemapper may have contained the head, but this led to no match. Or it didn't contain the head at all. In
* any case, check to see if it contains a * wildcard. * comes last in the AIML "alphabet".
*/
nextNodemapper = this.match(ASTERISK, // key
_matchState, // target match state for wildcard content
nodemapper, // current nodemapper
tail, // current tail
true, // append new path?
wildcardContent, // current wildcard content
head, // new wildcard content
path, // current path
match, // match object
_matchState, // current match state
expiration // expiration timestamp
);
if (nextNodemapper != null) {
return nextNodemapper;
}
/*
* The nodemapper has failed to match at all: it contains neither _, nor the head, nor *. However, if its parent is
* a wildcard, then the match continues to be valid and can proceed with the tail, the current path, and the star
* content plus the head as the new star.
*/
if (nodemapper.equals(parent.get(ASTERISK)) || nodemapper.equals(parent.get(UNDERSCORE))) {
nextNodemapper = this.match(nodemapper, // current nodemapper
parent, // current path
tail, // current tail
String.format("%s %s", wildcardContent, head), // head = wildcard content + head
path, // current path
match, // match object
_matchState, // current match state
expiration // expiration timestamp
);
if (nextNodemapper != null) {
return nextNodemapper;
}
}
/*
* If we get here, we've hit a dead end; this null value will be passed back up the recursive chain of matches,
* perhaps even hitting the high-level match method and causing a NoMatchException, though this is assumed to be the
* rarest occurence.
*/
return null;
}
/**
* An internal method used for matching. This method <i>assumes</i> that nodemapper.containsKey(key)!
*
* @param key
* @param wildcardDestination
* @param nodemapper
* @param tail
* @param appendToPath
* @param currentWildcard
* @param newWildcard
* @param path
* @param match
* @param matchState
* @param expiration
* @return the resulting leaf nodemapper
* @throws NoMatchException
*/
protected Nodemapper match(String key, Match.State wildcardDestination, Nodemapper nodemapper, List<String> tail,
boolean appendToPath, String currentWildcard, String newWildcard, StringBuilder path, Match match,
Match.State matchState, long expiration) throws NoMatchException {
// Construct a new path from the current path plus the key.
StringBuilder newPath = new StringBuilder();
if (path.length() > 0) {
newPath.append(path);
newPath.append(' ');
}
newPath.append(key);
// Try to get a match with the tail and this new path (may throw exception)
Nodemapper result = this.match((Nodemapper) nodemapper.get(key), // newly matched nodemapper
nodemapper, // current nodemapper as parent
tail, // current tail
newWildcard, // current wildcardContent
appendToPath ? newPath : new StringBuilder(), // either the new path, or a blank one
match, // match object
matchState, // current match state
expiration // expiration timestamp
);
// capture and push the wildcard content appropriate to the current match state.
if (wildcardDestination != null && wildcardDestination.compareTo(Match.State.IN_BOTID) < 0
&& currentWildcard.length() > 0) {
match.pushWildcardContent(wildcardDestination, currentWildcard);
}
return result;
}
/**
* @param input
* @param that
* @param topic
* @param botid
* @return the match
* @see org.aitools.programd.graph.Graphmapper#match(java.lang.String, java.lang.String, java.lang.String,
* java.lang.String)
* @throws NoMatchException
*/
@Override
public Match match(String input, String that, String topic, String botid) throws NoMatchException {
// Get the match, starting at the root, with an empty star and path, starting in "in input" mode.
Match match = new Match();
Nodemapper result = this.match(this.root, this.root, AbstractGraphmapper.composeInputPath(input, that, topic, botid), "",
new StringBuilder(), match, Match.State.IN_INPUT, System.currentTimeMillis() + this._responseTimeout);
if (result != null) {
return match;
}
throw new NoMatchException(String.format("%s:%s:%s:%s", input, that, topic, botid));
}
private void print(Nodemapper nodemapper, PrintWriter out) {
ArrayList<String> keyList = new ArrayList<String>(nodemapper.keySet());
int keyCount = keyList.size();
for (int index = 0; index < keyCount; index++) {
String key = keyList.get(index);
out.print(key);
out.print(' ');
Object value = nodemapper.get(key);
if (value instanceof Nodemapper) {
this.print((Nodemapper) value, out);
}
else {
out.print(org.jdom.Text.normalizeString((String) value));
if (index == keyCount - 1) {
out.println();
}
}
}
}
@Override
protected void print(PrintWriter out) {
this.print(this.root, out);
out.close();
}
/**
* Removes a node, as well as as many of its ancestors as have no descendants other than this nodemapper or its
* ancestors.
*
* @param nodemapper the mapper for the nodemapper to remove
*/
protected void remove(Nodemapper nodemapper) {
Nodemapper parent = nodemapper.getParent();
if (parent != null) {
parent.remove(nodemapper);
if (parent.size() == 0 && parent != this.root) {
this.remove(parent);
}
}
}
/**
* @see org.aitools.programd.graph.Graphmapper#removeCategory(java.lang.String, java.lang.String, java.lang.String,
* org.aitools.programd.Bot)
*/
@Override
public void removeCategory(String pattern, String that, String topic, Bot bot) {
Nodemapper nodemapper = null;
try {
nodemapper = this.match(this.root, this.root, AbstractGraphmapper.composeInputPath(pattern, that, topic, bot.getID()), "",
new StringBuilder(), new Match(), Match.State.IN_INPUT, System.currentTimeMillis() + this._responseTimeout);
}
catch (NoMatchException e) {
throw new DeveloperError("Could not remove category.", e);
}
if (nodemapper != null) {
this.remove(nodemapper);
}
else {
this._logger.error(String.format("Could not find category to remove (%s:%s:%s)", pattern, that, topic, bot));
}
}
/**
* @see org.aitools.programd.graph.Graphmapper#unload(java.net.URL, org.aitools.programd.Bot)
*/
@Override
public void unload(URL path, Bot bot) {
Set<Nodemapper> nodemappers = bot.getLoadedFilesMap().get(path);
for (Nodemapper nodemapper : nodemappers) {
this.remove(nodemapper);
this._totalCategories--;
}
nodemappers.clear();
Set<String> botids = this._urlCatalog.get(path);
// It can end up being null if there was an error in loading
// (non-existent file).
if (botids != null) {
botids.remove(bot.getID());
}
if (botids == null || botids.size() == 0) {
this._urlCatalog.remove(path);
}
}
}