/* * 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.parser; import java.io.FileNotFoundException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import org.aitools.programd.Bot; import org.aitools.programd.Bots; import org.aitools.programd.Core; import org.aitools.programd.graph.Graphmapper; import org.aitools.programd.listener.InvalidListenerParameterException; import org.aitools.programd.listener.Listener; import org.aitools.util.Classes; import org.aitools.util.resource.URLTools; import org.aitools.util.runtime.DeveloperError; import org.aitools.util.runtime.UserError; import org.aitools.util.xml.JDOM; import org.apache.log4j.Logger; import org.jdom.DataConversionException; import org.jdom.Element; import org.jdom.Namespace; /** * <code>BotsConfigurationFileParser</code> processes a bots configuration file. */ public class BotsConfigurationFileParser { /** Enum for specifying substitution type. */ static enum SubstitutionType { /** an input substitution */ INPUT, /** a gender substitution */ GENDER, /** a person substitution */ PERSON, /** a person2 substitution */ PERSON2; /** * @param name * @return the type that corresponds with the given name */ public static SubstitutionType get(String name) { if (name.equals("input")) { return INPUT; } else if (name.equals("gender")) { return GENDER; } else if (name.equals("person")) { return PERSON; } else if (name.equals("person2")) { return PERSON2; } throw new IllegalArgumentException(String.format("\"%s\" is not a valid substitution type.", name)); } } /** The namespace processed by this version of the parser. */ public static final String NAMESPACE_URI = "http://aitools.org/programd/4.7/bot-configuration"; /** A JDOM namespace object used in processing. */ private static final Namespace NS = Namespace.getNamespace(NAMESPACE_URI); private Core _core; private Logger _logger; /** * Initializes a <code>BotsConfigurationFileParser</code>. * * @param core */ public BotsConfigurationFileParser(Core core) { this._core = core; this._logger = core.getLogger(); } /** * A common method used in this class for loading a document and getting its root element. * * @param path * @return the root element of the document at the given path */ protected Element getDocRoot(URL path) { return JDOM.getDocument(path, this._core.getSettings().getXmlCatalogPath(), this._core.getLogger()).getRootElement(); } @SuppressWarnings("unchecked") protected void loadAIML(Bot bot, Element element) { for (Element learn : (List<Element>) element.getChildren("learn", NS)) { this._core.load(JDOM.contextualize(learn.getText(), element), bot.getID()); } } @SuppressWarnings("boxing") protected void loadBot(Element element) { if (element.getAttribute("href") != null) { this.parse(JDOM.contextualize(element.getAttributeValue("href"), element)); } else { String botid = element.getAttributeValue("id"); boolean enabled = false; try { enabled = element.getAttribute("enabled").getBooleanValue(); } catch (DataConversionException e) { assert false : "Schema did not catch invalid valid for \"enabled\" attribute."; } if (enabled) { Bots bots = this._core.getBots(); if (bots.containsKey(botid)) { this._logger.warn(String.format("Bot \"%s\" has already been configured.", botid)); return; } Bot bot = new Bot(botid, this._core.getSettings()); this._logger.info(String.format("Configuring bot \"%s\".", botid)); bots.put(botid, bot); Graphmapper graphmapper = this._core.getGraphmapper(); int previousCategoryCount = graphmapper.getCategoryCount(); int previousDuplicateCount = graphmapper.getDuplicateCategoryCount(); // Stop the AIMLWatcher while loading. if (this._core.getSettings().useAIMLWatcher()) { this._core.getAIMLWatcher().stop(); } // Index the start time before loading. long time = new Date().getTime(); // Load the bot. this.loadConfig(bot, element, "properties"); this.loadConfig(bot, element, "predicates"); this.loadConfig(bot, element, "substitutions"); this.loadConfig(bot, element, "sentence-splitters"); this.loadConfig(bot, element, "listeners"); this.loadConfig(bot, element, "testing"); this.loadAIML(bot, element); // Calculate the time used to load all categories. time = new Date().getTime() - time; // Restart the AIMLWatcher. if (this._core.getSettings().useAIMLWatcher()) { this._core.getAIMLWatcher().start(); } this._logger.info(String.format("%,d categories loaded in %.4f seconds.", graphmapper.getCategoryCount() - previousCategoryCount, time / 1000.00)); this._logger.info(graphmapper.getCategoryReport()); int dupes = graphmapper.getDuplicateCategoryCount() - previousDuplicateCount; if (dupes > 0) { this._logger.warn(String.format( "%,d path-identical categories were encountered, and handled according to the %s merge policy.", dupes, this._core.getSettings().getMergePolicy())); } } } } /** * A generic method for loading configuration data. * * @param bot the bot object into which to load data * @param parent the parent "bot" element * @param name the name of the child element from which to load config data; is used to determine method name to call * to load config data */ protected void loadConfig(Bot bot, Element parent, String name) { Element child = parent.getChild(name, NS); if (child != null) { String methodName = String.format("load%s%s", name.substring(0, 1).toUpperCase(), name.substring(1).replaceAll("\\W", "")); Method method; try { method = this.getClass().getDeclaredMethod(methodName, new Class[] { Bot.class, Element.class }); } catch (SecurityException e) { throw new DeveloperError(String.format("Cannot access method %s", methodName), e); } catch (NoSuchMethodException e) { throw new DeveloperError(String.format("No such method %s", methodName), e); } try { if (child.getAttribute("href") != null) { method.invoke(this, bot, this.getDocRoot(JDOM.contextualize(child.getAttributeValue("href"), child))); } else { method.invoke(this, bot, child); } } catch (InvocationTargetException e) { throw new DeveloperError(String.format("Could not invoke method %s", methodName), e); } catch (IllegalAccessException e) { throw new DeveloperError(String.format("Could not invoke method %s", methodName), e); } } } @SuppressWarnings("unchecked") protected void loadListeners(Bot bot, Element element) { for (Element listenerElement : (List<Element>) element.getChildren("listener", NS)) { // Enabled? boolean enabled = false; try { enabled = listenerElement.getAttribute("enabled").getBooleanValue(); } catch (DataConversionException e1) { assert false : "Schema did not catch invalid value for \"enabled\" attribute."; } if (!enabled) { return; } // Set up the parameters for the listener. Map<String, String> parameters = new HashMap<String, String>(); for (Element parameter : (List<Element>) listenerElement.getChildren()) { parameters.put(parameter.getAttributeValue("name"), parameter.getAttributeValue("value")); } // Instantiate a new listener for the bot. String classname = listenerElement.getAttributeValue("class"); Listener listener = Classes.getSubclassInstance(Listener.class, classname, "listener", this._core, bot, parameters); // Check listener parameters. try { listener.checkParameters(); } catch (InvalidListenerParameterException e) { throw new UserError("Listener is not properly configured!", e); } // Start listener this._core.getManagedProcesses().start(listener, String.format("%s : %s", classname, bot.getID())); this._logger.info(String.format("Started \"%s\" listener for bot \"%s\".", classname, bot.getID())); } } @SuppressWarnings("unchecked") protected static void loadPredicates(Bot bot, Element element) { for (Element predicate : (List<Element>) element.getChildren("predicate", NS)) { String name = predicate.getAttributeValue("name"); String defaultValue = predicate.getAttributeValue("default"); if ("".equals(defaultValue)) { defaultValue = null; } String setReturn = predicate.getAttributeValue("set-return"); boolean returnNameWhenSet = false; if (setReturn.equals("name")) { returnNameWhenSet = true; } bot.addPredicateInfo(name, defaultValue, returnNameWhenSet); } } @SuppressWarnings("unchecked") protected static void loadProperties(Bot bot, Element element) { for (Element property : (List<Element>) element.getChildren("property", NS)) { bot.setPropertyValue(property.getAttributeValue("name"), property.getAttributeValue("value")); } } @SuppressWarnings("unchecked") protected static void loadSentencesplitters(Bot bot, Element element) { for (Element splitter : (List<Element>) element.getChildren("splitter", NS)) { bot.addSentenceSplitter(splitter.getAttributeValue("value")); } } @SuppressWarnings("unchecked") protected static void loadSubstitutions(Bot bot, Element element) { for (Element substitutionTypes : (List<Element>) element.getChildren()) { SubstitutionType type = SubstitutionType.get(substitutionTypes.getName()); for (Element substitution : (List<Element>) substitutionTypes.getChildren()) { String find = substitution.getAttributeValue("find"); // Compile the find pattern. Pattern pattern; try { pattern = Pattern.compile(find, Pattern.CANON_EQ | Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); } catch (PatternSyntaxException e) { throw new UserError(String.format("Invalid substitution pattern \"%s\".", find), e); } String replace = substitution.getAttributeValue("replace"); switch (type) { case INPUT: bot.addInputSubstitution(pattern, replace); break; case GENDER: bot.addSubstitution(org.aitools.programd.processor.aiml.GenderProcessor.class, pattern, replace); break; case PERSON: bot.addSubstitution(org.aitools.programd.processor.aiml.PersonProcessor.class, pattern, replace); break; case PERSON2: bot.addSubstitution(org.aitools.programd.processor.aiml.Person2Processor.class, pattern, replace); break; } } } } protected static void loadTesting(Bot bot, Element element) { URL docURL; try { docURL = URLTools.createValidURL(element.getDocument().getBaseURI()); } catch (FileNotFoundException e) { throw new DeveloperError("Could not get bot config document URL when setting up testing.", e); } bot.setTestSuitePathspec(URLTools.getURLs(element.getChildText("test-suite-path", NS), docURL)); bot.setTestReportDirectory(URLTools.contextualize(docURL, element.getChildText("report-directory", NS))); } /** * Loads the bot config from the given path. * * @param path */ @SuppressWarnings("unchecked") public void parse(URL path) { Element root = this.getDocRoot(path); if (root.getName().equals("bots")) { for (Element bot : (List<Element>) root.getChildren("bot", NS)) { this.loadBot(bot); } } else if (root.getName().equals("bot")) { this.loadBot(root); } else { throw new IllegalArgumentException(String.format("Invalid bot config file \"%s\".", path)); } } }