/* * 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.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; import java.io.StringReader; import java.net.URL; import java.util.ArrayList; import java.util.List; import org.aitools.programd.Bot; import org.aitools.programd.Core; import org.aitools.programd.CoreSettings; import org.aitools.programd.parser.AIMLReader; import org.aitools.programd.processor.aiml.RandomProcessor; import org.aitools.util.Text; import org.aitools.util.resource.Filesystem; import org.aitools.util.resource.URLTools; import org.aitools.util.runtime.Errors; import org.aitools.util.runtime.UserError; import org.aitools.util.xml.SAX; import org.apache.log4j.Logger; import org.jdom.Content; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.input.SAXBuilder; import org.jdom.output.Format; import org.jdom.output.XMLOutputter; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; /** * @author <a href="mailto:noel@aitools.org">Noel Bush</a> */ abstract public class AbstractGraphmapper implements Graphmapper { // Instance variables. /** The Core with which this Graphmapper is associated. */ protected Core _core; /** The logger. */ protected Logger _logger = Logger.getLogger("programd"); /** The match logger. */ protected Logger _matchLogger = Logger.getLogger("programd.matching"); /** The merge policy. */ protected CoreSettings.MergePolicy _mergePolicy; /** A formatter used for outputting XML. */ private Format _xmlFormat = Format.getPrettyFormat(); /** Whether to note each file loaded. */ protected boolean _noteEachLoad; /** The separator string to use with the "append" merge policy. */ protected String _mergeAppendSeparator; /** Whether to note each merge. */ protected boolean _noteEachMerge; /** Whether to use the AIML Watcher. */ protected boolean _useAIMLWatcher; /** The AIML namespace URI in use. */ protected String _aimlNamespaceURI; /** How frequently to provide a category load count. */ protected int _categoryLoadNotifyInterval; /** The total number of categories read. */ protected int _totalCategories = 0; /** The total number of path-identical categories that have been encountered. */ protected int _duplicateCategories = 0; /** The response timeout. */ protected int _responseTimeout; // Constants /** A that marker. */ public static final String THAT = "<that>"; /** A topic marker. */ public static final String TOPIC = "<topic>"; /** A bot marker. */ public static final String BOT = "<bot>"; /** A template marker. */ public static final String TEMPLATE = "<template>"; /** A filename marker. */ public static final String FILENAME = "<filename>"; /** The <code>*</code> wildcard. */ public static final String ASTERISK = "*"; /** The <code>_</code> wildcard. */ public static final String UNDERSCORE = "_"; /** * Creates a new AbstractGraphmapper, reading settings from the given Core. * * @param core the CoreSettings object from which to read settings */ protected AbstractGraphmapper(Core core) { this._core = core; CoreSettings settings = this._core.getSettings(); this._noteEachLoad = settings.noteEachLoadedFile(); this._mergePolicy = settings.getMergePolicy(); this._mergeAppendSeparator = settings.getAppendMergeSeparatorString(); this._noteEachMerge = settings.noteEachMerge(); this._useAIMLWatcher = settings.useAIMLWatcher(); this._responseTimeout = settings.getResponseTimeout(); this._categoryLoadNotifyInterval = settings.getCategoryLoadNotificationInterval(); this._aimlNamespaceURI = settings.getAIMLNamespaceURI().toString(); } protected abstract void add(String pattern, String that, String topic, String template, Bot bot, URL 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 @SuppressWarnings("boxing") public void addCategory(String pattern, String that, String topic, String template, Bot bot, URL source) { // Make sure the path components are right. String _pattern = pattern == null ? ASTERISK : pattern; String _that = that == null ? ASTERISK : that; String _topic = topic == null ? ASTERISK : topic; // Report on loaded categories. if (this._totalCategories % this._categoryLoadNotifyInterval == 0 && this._totalCategories > 0) { this._logger.info(String.format("%,d categories loaded so far.", this._totalCategories)); } this.add(_pattern, _that, _topic, template, bot, source); } /** * Adds the given botid to the <botid> node for all branches associated with the given URL. This should only be * called using a URL that <i>has</i> previously been loaded for <i>another</i> bot. * * @param path * @param botid * @throws IllegalArgumentException if the given path has not already been loaded, or if it has been loaded for the * same botid */ abstract protected void addForBot(URL path, String botid); /** * Appends the contents of one template to another. * * @param existingTemplate the template to which to append * @param newTemplate the template whose content should be appended * @return the combined result */ @SuppressWarnings("unchecked") protected String appendTemplate(String existingTemplate, String newTemplate) { Document existingDoc; Element existingRoot; Document newDoc; List<Content> newContent; try { existingDoc = new SAXBuilder().build(new StringReader(existingTemplate)); existingRoot = existingDoc.getRootElement(); newDoc = new SAXBuilder().build(new StringReader(newTemplate)); newContent = newDoc.getRootElement().getContent(); } catch (JDOMException e) { this._logger.error("JDOM exception when performing merge append.", e); return existingTemplate; } catch (IOException e) { this._logger.error("IO exception when performing merge append.", e); return existingTemplate; } // Append whatever text is configured to be inserted between the templates. if (this._mergeAppendSeparator != null) { existingRoot.addContent(this._mergeAppendSeparator); } existingRoot.addContent(newContent); return new XMLOutputter(this._xmlFormat).outputString(existingDoc); } /** * Creates an association between the given botid and the given filename. * * @param botid * @param filename */ abstract protected void associateBotIDWithFilename(String botid, URL filename); /** * Combines two template content strings into a single template, using a random element so that either original * template content string has an equal chance of being processed. The order in which the templates are supplied is * important: the first one (<code>existingTemplate</code>) is processed as though it has already been stored in the * Graphmaster, and hence might itself be the result of a previous <code>combine()</code> operation. If this is the * case, the in-memory representation of the template will have a special attribute indicating this fact, which will * be used to "balance" the combine operation. * * @param existingTemplate the template with which the new template should be combined * @param newTemplate the template which should be combined with the existing template * @return the combined result */ @SuppressWarnings("unchecked") protected String combineTemplates(String existingTemplate, String newTemplate) { Document existingDoc; Element existingRoot; List<Content> existingContent; Document newDoc; List<Content> newContent = new ArrayList<Content>(); try { existingDoc = new SAXBuilder().build(new StringReader(existingTemplate)); existingRoot = existingDoc.getRootElement(); existingContent = existingRoot.getContent(); newDoc = new SAXBuilder().build(new StringReader(newTemplate)); for (Content newContentItem : (List<Content>) newDoc.getRootElement().getContent()) { newContent.add((Content) newContentItem.clone()); } } catch (JDOMException e) { this._logger.error("JDOM exception when performing merge combine.", e); return existingTemplate; } catch (IOException e) { this._logger.error("IO exception when performing merge combine.", e); return existingTemplate; } /* * If the existing template has a random element as its root, we need to check whether this was the result of a * previous combine. */ Content firstNode = existingContent.get(0); if (firstNode instanceof Element) { Element firstElement = (Element) firstNode; if (firstElement.getName().equals(RandomProcessor.label) && firstElement.getAttribute("synthetic") != null) { Element newListItem = new Element(RandomProcessor.LI, this._aimlNamespaceURI); newListItem.addContent(newContent); firstElement.addContent(newListItem); } return new XMLOutputter(this._xmlFormat).outputString(existingDoc); } Element listItemForExisting = new Element(RandomProcessor.LI, this._aimlNamespaceURI); existingRoot.removeContent(); listItemForExisting.addContent(existingContent); Element listItemForNew = new Element(RandomProcessor.LI, this._aimlNamespaceURI); listItemForNew.addContent(newContent); Element newRandom = new Element(RandomProcessor.label, this._aimlNamespaceURI); newRandom.setAttribute("synthetic", "yes"); newRandom.addContent(listItemForExisting); newRandom.addContent(listItemForNew); existingRoot.addContent(newRandom); return new XMLOutputter(this._xmlFormat).outputString(existingDoc); } /** * Composes an input path as a list of tokens, given the components. Empty components are represented with asterisks. * * @param input * @param that * @param topic * @param botid * @return the new path */ protected static List<String> composeInputPath(String input, String that, String topic, String botid) { List<String> inputPath = new ArrayList<String>(); // Input text part. if (input.length() > 0) { inputPath = Text.wordSplit(input); } else { inputPath = new ArrayList<String>(); inputPath.add(ASTERISK); } // <that> marker. inputPath.add(THAT); // Input <that> part. if (that.length() > 0) { inputPath.addAll(Text.wordSplit(that)); } else { inputPath.add(ASTERISK); } // <topic> marker. inputPath.add(TOPIC); // Input <topic> part. if (topic.length() > 0) { inputPath.addAll(Text.wordSplit(topic)); } else { inputPath.add(ASTERISK); } // <botid> marker. inputPath.add(BOT); // Input [directed to] botid. inputPath.add(botid); return inputPath; } protected void doLoad(URL path, String botid) { this.beforeLoad(path, botid); AIMLReader handler = new AIMLReader(this, path, this._core.getBot(botid)); XMLReader reader = SAX.getReader(handler, this._logger, this._core.getSettings().getXmlCatalogPath()); try { reader.parse(path.toExternalForm()); this.associateBotIDWithFilename(botid, path); } catch (IOException e) { this._logger.warn(String.format("Error reading \"%s\": %s", URLTools.unescape(path), Errors.describe(e)), e); } catch (SAXException e) { this._logger.warn(String.format("Error reading \"%s\": %s", URLTools.unescape(path), Errors.describe(e))); } this.afterLoad(path, botid); } /** * @see org.aitools.programd.graph.Graphmapper#beforeLoad(java.net.URL, java.lang.String) */ @Override public void beforeLoad(URL path, String botid) { // Nothing done at this level, but some Graphmappers will want to do something. } /** * @see org.aitools.programd.graph.Graphmapper#afterLoad(java.net.URL, java.lang.String) */ @Override public void afterLoad(URL path, String botid) { // Nothing done at this level, but some Graphmappers will want to do something. } /** * @see org.aitools.programd.graph.Graphmapper#getCategoryCount() */ @Override public int getCategoryCount() { return this._totalCategories; } /** * @see org.aitools.programd.graph.Graphmapper#getCategoryReport() */ @Override @SuppressWarnings("boxing") public String getCategoryReport() { return String.format("%,d total categories currently loaded.", this._totalCategories); } /** * @see org.aitools.programd.graph.Graphmapper#getDuplicateCategoryCount() */ @Override public int getDuplicateCategoryCount() { return this._duplicateCategories; } /** * Indicates whether the given filename is already loaded for any bot at all. * * @param filename * @return whether the given filename is already loaded */ abstract protected boolean isAlreadyLoaded(URL filename); /** * Indicates whether the given filename is already loaded for the given bot. * * @param filename * @param botid * @return whether the given filename is already loaded for the given botid */ abstract protected boolean isAlreadyLoadedForBot(URL filename, String botid); /** * @see org.aitools.programd.graph.Graphmapper#load(java.net.URL, java.lang.String) */ @Override public void load(URL path, String botid) { // Handle paths with wildcards that need to be expanded. if (path.getProtocol().equals(Filesystem.FILE)) { String spec = path.getFile(); if (spec.indexOf('*') != -1) { List<File> files = null; try { files = Filesystem.glob(spec); } catch (FileNotFoundException e) { this._logger.warn(e.getMessage()); } if (files != null) { for (File file : files) { this.load(URLTools.contextualize(URLTools.getParent(path), file.getAbsolutePath()), botid); } } return; } } Bot bot = this._core.getBot(botid); // Let the Graphmapper use a shortcut if possible. if (this.isAlreadyLoaded(path)) { if (this.isAlreadyLoadedForBot(path, botid)) { if (this._logger.isDebugEnabled()) { this._logger.debug(String.format("Reloading \"%s\" for \"%s\" (is that what you wanted?).", path, botid)); } this.unload(path, bot); this.doLoad(path, botid); } else { if (this._logger.isDebugEnabled()) { this._logger.debug(String.format("Graphmapper has already loaded \"%s\" for some other bot.", path)); } this.addForBot(path, botid); } } else { if (this._noteEachLoad) { this._logger.info(String.format("Loading %s....", URLTools.unescape(path))); } this.doLoad(path, botid); // Add it to the AIMLWatcher, if active. if (this._useAIMLWatcher) { this._core.getAIMLWatcher().addWatchFile(path); } } } abstract protected void print(PrintWriter out); /** * @see org.aitools.programd.graph.Graphmapper#print(java.lang.String) */ @Override public void print(String path) { try { this.print(new PrintWriter(Filesystem.checkOrCreate(path, "Graphmapper output"))); } catch (FileNotFoundException e) { throw new UserError("Cannot find file to print graph.", e); } } }