/**
* Copyright (c) 2005-2017, KoLmafia development team
* http://kolmafia.sourceforge.net/
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* [1] Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* [2] Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* [3] Neither the name "KoLmafia" nor the names of its contributors may
* be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package net.sourceforge.kolmafia.persistence;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import net.java.dev.spellcast.utilities.LockableListModel;
import net.java.dev.spellcast.utilities.SortedListModel;
import net.sourceforge.kolmafia.KoLConstants;
import net.sourceforge.kolmafia.KoLConstants.MafiaState;
import net.sourceforge.kolmafia.KoLmafia;
import net.sourceforge.kolmafia.RequestLogger;
import net.sourceforge.kolmafia.RequestThread;
import net.sourceforge.kolmafia.StaticEntity;
import net.sourceforge.kolmafia.session.ContactManager;
import net.sourceforge.kolmafia.utilities.CharacterEntities;
import net.sourceforge.kolmafia.utilities.FileUtilities;
import net.sourceforge.kolmafia.utilities.StringUtilities;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class FaxBotDatabase
{
private static boolean isInitialized = false;
private static boolean faxBotError = false;
private static String faxBotErrorMessage = "";
// List of bots from faxbots.txt
public static final ArrayList<BotData> botData = new ArrayList<BotData>();
// List of faxbots named in config files.
public static final ArrayList<FaxBot> faxbots = new ArrayList<FaxBot>();
public static final void reconfigure()
{
FaxBotDatabase.isInitialized = false;
FaxBotDatabase.configure();
}
public static final void configure()
{
if ( FaxBotDatabase.isInitialized )
{
return;
}
FaxBotDatabase.readFaxbotConfig();
FaxBotDatabase.configureFaxBots();
}
private static final void readFaxbotConfig()
{
FaxBotDatabase.botData.clear();
BufferedReader reader =
FileUtilities.getVersionedReader( "faxbots.txt", KoLConstants.FAXBOTS_VERSION );
String[] data;
while ( ( data = FileUtilities.readData( reader ) ) != null )
{
if ( data.length > 1 )
{
FaxBotDatabase.botData.add( new BotData( data[ 0 ].trim().toLowerCase(), data[ 1 ].trim() ) );
}
}
try
{
reader.close();
}
catch ( Exception e )
{
// This should not happen. Therefore, print
// a stack trace for debug purposes.
StaticEntity.printStackTrace( e );
}
}
private static final void configureFaxBots()
{
KoLmafia.updateDisplay( "Configuring faxable monsters." );
FaxBotDatabase.faxbots.clear();
for ( BotData data : FaxBotDatabase.botData )
{
FaxBotDatabase.configureFaxBot( data );
}
KoLmafia.updateDisplay( "Faxable monster lists fetched." );
FaxBotDatabase.isInitialized = true;
}
private static final void configureFaxBot( final BotData data )
{
FaxBotDatabase.faxBotError = false;
FaxBotDatabase.faxBotErrorMessage = "";
KoLmafia.forceContinue();
RequestThread.postRequest( new DynamicBotFetcher( data ) );
if ( FaxBotDatabase.faxBotError )
{
KoLmafia.updateDisplay( MafiaState.ABORT, "Could not load " + data.name + " configuration from \"" + data.URL + "\"" );
RequestLogger.printLine( FaxBotDatabase.faxBotErrorMessage );
return;
}
}
public static final FaxBot getFaxbot( final int i )
{
return ( i < 0 || i >= faxbots.size() ) ? null : FaxBotDatabase.faxbots.get(i);
}
public static final String botName( final int i )
{
FaxBot bot = FaxBotDatabase.getFaxbot( i );
return bot == null ? null : bot.name;
}
public static class BotData
{
public final String name;
public final String URL;
public BotData( final String name, final String URL )
{
this.name = name;
this.URL = URL;
}
}
public static class FaxBot
implements Comparable<FaxBot>
{
// Who is this bot?
private final String name;
private final int playerId;
// What monsters does it serve?
public final SortedListModel<Monster> monsters = new SortedListModel<Monster>();
// Lists derived from the list of monsters
private final LockableListModel<String> categories = new LockableListModel<String>();
private LockableListModel<Monster> [] monstersByCategory = new LockableListModel[0];
private final Map<String, Monster> monsterByActualName = new HashMap<String, Monster>();
private final Map<String, Monster> monsterByCommand = new HashMap<String, Monster>();
private String[] canonicalCommands;
public FaxBot( final String name, final String playerId )
{
this( name, StringUtilities.parseInt( playerId ) );
}
public FaxBot( final String name, final int playerId )
{
this.name = name;
this.playerId = playerId;
}
public String getName()
{
return this.name;
}
public int getPlayerId()
{
return this.playerId;
}
public LockableListModel<String> getCategories()
{
return this.categories;
}
public LockableListModel<Monster> [] getMonstersByCategory()
{
return this.monstersByCategory;
}
public Monster getMonsterByActualName( final String actualName )
{
return this.monsterByActualName.get( StringUtilities.getCanonicalName( actualName ) );
}
public Monster getMonsterByCommand( final String command )
{
return this.monsterByCommand.get( StringUtilities.getCanonicalName( command ) );
}
public void addMonsters( final List<Monster> monsters )
{
// Build the list of monsters and derived mappings
this.monsters.clear();
this.monsterByActualName.clear();
this.monsterByCommand.clear();
SortedListModel<String> tempCategories = new SortedListModel<String>();
for ( Monster monster : monsters )
{
this.monsters.add( monster );
String category = monster.category;
if ( !category.equals( "" ) && !category.equalsIgnoreCase( "none" ) && !tempCategories.contains( category ) )
{
tempCategories.add( category );
}
// Build actual name / command lookup
String canonicalName = StringUtilities.getCanonicalName( monster.actualName );
this.monsterByActualName.put( canonicalName, monster );
String canonicalCommand = StringUtilities.getCanonicalName( monster.command );
this.monsterByCommand.put( canonicalCommand, monster );
}
// Create the canonical command list
Set<String> commands = this.monsterByCommand.keySet();
String[] array = new String[0];
this.canonicalCommands = commands.toArray( array );
Arrays.sort( this.canonicalCommands );
this.categories.clear();
this.categories.add( "All Monsters" );
this.categories.addAll( tempCategories );
// Make one list for each category
this.monstersByCategory = new SortedListModel[ this.categories.size() ];
for ( int i = 0; i < this.categories.size(); ++i )
{
String category = (String)categories.get( i );
SortedListModel<Monster> model = new SortedListModel<Monster>();
this.monstersByCategory[ i ] = model;
for ( Monster monster : monsters )
{
if ( i == 0 || category.equals( monster.category ) )
{
model.add( monster );
}
}
}
}
public List findMatchingCommands( final String command )
{
String canonical = StringUtilities.getCanonicalName( command );
return StringUtilities.getMatchingNames( this.canonicalCommands, canonical );
}
@Override
public boolean equals( final Object o )
{
if ( o == null || !( o instanceof FaxBot ) )
{
return false;
}
FaxBot that = (FaxBot) o;
return this.name.equals( that.name );
}
@Override
public int hashCode()
{
return this.name != null ? this.name.hashCode() : 0;
}
public int compareTo( final FaxBot o )
{
if ( o == null || !( o instanceof FaxBot ) )
{
return -1;
}
FaxBot that = (FaxBot) o;
return this.name.compareTo( that.name );
}
}
public static class Monster
implements Comparable<Monster>
{
private final String name;
private final String actualName;
private final String command;
private final String category;
private final String stringForm;
private final String lowerCaseStringForm;
public Monster( final String name, final String actualName, final String command, final String category )
{
this.name = CharacterEntities.unescape( name );
this.actualName = CharacterEntities.unescape( actualName );
this.command = command;
this.category = category;
this.stringForm = this.name + " [" + command + "]";
this.lowerCaseStringForm = this.stringForm.toLowerCase();
}
public String getName()
{
return this.name;
}
public String getActualName()
{
return this.actualName;
}
public String getCommand()
{
return this.command;
}
public String getCategory()
{
return this.category;
}
@Override
public String toString()
{
return this.stringForm;
}
public String toLowerCaseString()
{
return this.lowerCaseStringForm;
}
@Override
public boolean equals( final Object o )
{
if ( o == null || !( o instanceof Monster ) )
{
return false;
}
Monster that = (Monster) o;
return this.name.equals( that.name );
}
@Override
public int hashCode()
{
return this.name != null ? this.name.hashCode() : 0;
}
public int compareTo( final Monster o )
{
if ( o == null || !( o instanceof Monster ) )
{
return -1;
}
Monster that = (Monster) o;
return this.name.compareToIgnoreCase( that.name );
}
}
private static class DynamicBotFetcher
implements Runnable
{
private final BotData data;
public DynamicBotFetcher( final BotData data )
{
this.data = data;
}
public void run()
{
// Start with a clean slate
FaxBotDatabase.faxBotError = false;
FaxBotDatabase.faxBotErrorMessage = "";
KoLmafia.forceContinue();
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
Document dom = null;
try
{
File local = new File( KoLConstants.DATA_LOCATION, this.data.name + ".xml" );
FileUtilities.downloadFile( this.data.URL, local, true );
// Get an instance of document builder
DocumentBuilder db = dbf.newDocumentBuilder();
// Parse using builder to get DOM
// representation of the XML file
dom = db.parse( local );
}
catch (ParserConfigurationException pce)
{
FaxBotDatabase.faxBotErrorMessage = pce.getMessage();
}
catch (SAXException se)
{
FaxBotDatabase.faxBotErrorMessage = se.getMessage();
}
catch (IOException ioe)
{
FaxBotDatabase.faxBotErrorMessage = ioe.getMessage();
}
if ( dom == null )
{
FaxBotDatabase.faxBotError = true;
return;
}
Element doc = dom.getDocumentElement();
// Get a nodelist of bots
ArrayList<FaxBot> bots = new ArrayList<FaxBot>();
NodeList bl = doc.getElementsByTagName( "botdata" );
if ( bl != null )
{
for ( int i = 0; i < bl.getLength(); i++ )
{
Element el = (Element)bl.item( i );
FaxBot fb = getFaxBot( el );
bots.add( fb );
}
}
// Get a nodelist of monsters
NodeList fl = doc.getElementsByTagName( "monsterdata" );
ArrayList<Monster> monsters = new ArrayList<Monster>();
if ( fl != null )
{
for ( int i = 0; i < fl.getLength(); i++ )
{
Element el = (Element)fl.item( i );
Monster monster = getMonster( el );
if ( monster != null )
{
monsters.add( monster );
}
}
}
// For each bot, add available monsters
for ( FaxBot bot : bots )
{
bot.addMonsters( monsters );
}
// Add the bots to the list of available bots
FaxBotDatabase.faxbots.addAll( bots );
}
private FaxBot getFaxBot( Element el )
{
String name = getTextValue( el, "name" );
String playerId = getTextValue( el, "playerid" );
ContactManager.registerPlayerId( name, playerId );
KoLmafia.updateDisplay( "Configuring " + name + " (" + playerId + ")" );
return new FaxBot( name, playerId );
}
private Monster getMonster( Element el )
{
String monster = getTextValue( el, "name" );
if ( monster.equals( "" ) || monster.equals( "none" ) )
{
return null;
}
String actualMonster = getTextValue( el, "actual_name" );
if ( actualMonster.equals( "" ) )
{
return null;
}
String command = getTextValue( el, "command" );
if ( command.equals( "" ) )
{
return null;
}
String category = getTextValue( el, "category" );
return new Monster( monster, actualMonster, command, category );
}
private String getTextValue( Element ele, String tagName )
{
NodeList nl = ele.getElementsByTagName( tagName );
if ( nl != null && nl.getLength() > 0 )
{
Element el = (Element)nl.item(0);
return el.getFirstChild().getNodeValue();
}
return "";
}
}
}