/*
* ClassDataParser.java
* Copyright 2006 (C) Aaron Divinsky <boomer70@yahoo.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Current Ver: $Revision$
*/
package pcgen.core.npcgen;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import pcgen.cdom.base.AssociatedPrereqObject;
import pcgen.cdom.base.CDOMList;
import pcgen.cdom.base.CDOMReference;
import pcgen.cdom.base.MasterListInterface;
import pcgen.cdom.enumeration.AssociationKey;
import pcgen.cdom.enumeration.ListKey;
import pcgen.cdom.enumeration.ObjectKey;
import pcgen.cdom.enumeration.Type;
import pcgen.core.Ability;
import pcgen.core.AbilityCategory;
import pcgen.core.GameMode;
import pcgen.core.Globals;
import pcgen.core.PCClass;
import pcgen.core.PCStat;
import pcgen.core.PlayerCharacter;
import pcgen.core.SettingsHandler;
import pcgen.core.Skill;
import pcgen.core.SystemCollections;
import pcgen.core.prereq.PrereqHandler;
import pcgen.core.spell.Spell;
import pcgen.util.Logging;
import pcgen.util.enumeration.Visibility;
/**
* Parse a generator class data file.
*
* @author boomer70 <boomer70@yahoo.com>
*
*/
public class ClassDataParser
{
private SAXParser theParser;
private GameMode theMode;
/**
* Creates a new <tt>ClassDataParser</tt> for the specified game mode.
*
* @param aMode The game mode to parse class options for.
*
* @throws ParserConfigurationException
* @throws SAXException
*/
public ClassDataParser(final GameMode aMode)
throws ParserConfigurationException, SAXException
{
theMode = aMode;
final SAXParserFactory parserFactory = SAXParserFactory.newInstance();
parserFactory.setValidating(true);
theParser = parserFactory.newSAXParser();
}
/**
* Parses a XML class data options file.
*
* @param aFileName File to parse.
*
* @return A <tt>List</tt> of <tt>ClassData</tt> objects representing the
* options in the file.
*
* @throws SAXException
* @throws IOException
*/
public List<ClassData> parse( final File aFileName )
throws SAXException, IOException
{
final List<ClassData> ret = new ArrayList<>();
try
{
theParser.parse(aFileName, new ClassDataHandler(theMode, ret));
}
catch (IllegalArgumentException ex )
{
// Do nothing, means we weren't the right game mode for this file.
}
return ret;
}
}
/**
* This is the parsing event handler class. The methods in this class are
* called by the SAX parser as it finds various elements in the XML file.
*
* @author boomer70 <boomer70@yahoo.com>
*
*/
class ClassDataHandler extends DefaultHandler
{
private List<ClassData> theList;
private GameMode theGameMode = null;
private boolean theValidFlag = false;
/** An enum for the current state in the state machine the parser is in */
private enum ParserState
{
/** The initial state of the parser */
INIT,
/** Found a class tag */
CLASSDATA,
/** Found stat data */
STATDATA,
/** Found skill data */
SKILLDATA,
/** Found Ability data */
ABILITYDATA,
/** Found spell data */
SPELLDATA,
/** Found spells of a level */
SPELLLEVELDATA,
/** Found subclass data */
SUBCLASSDATA
}
private ParserState theState = ParserState.INIT;
private ClassData theCurrentData = null;
private AbilityCategory theCurrentCategory = null;
private enum SpellType
{
/** Adding Known spells */
KNOWN,
/** Adding Prepared Spells */
PREPARED
}
private SpellType theCurrentSpellType = SpellType.KNOWN;
private int theCurrentLevel = -1;
// Weight for any skills added from *
private transient int remainingWeight = -1;
private transient List<String> removeList = new ArrayList<>();
/**
* Constructs the handler
*
* @param aMode The game mode to expect the file to be for.
* @param aList The list of <tt>ClassData</tt> objects to fill
*/
public ClassDataHandler( final GameMode aMode, final List<ClassData> aList )
{
theGameMode = aMode;
theList = aList;
}
/**
* @throws SAXException
* @throws IllegalArgumentException if the file being processed is not the
* same GameMode as requested.
*
* @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
*/
@Override
public void startElement( final String uri, final String localName,
final String aName, final Attributes anAttrs)
throws SAXException
{
if ( theState == ParserState.INIT && "class_data".equals(aName) ) //$NON-NLS-1$
{
if ( anAttrs != null )
{
final String gm = anAttrs.getValue("game_mode"); //$NON-NLS-1$
if ( ! SystemCollections.getGameModeNamed(gm).equals(theGameMode) )
{
throw new IllegalArgumentException("Incorrect game mode"); //$NON-NLS-1$
}
theValidFlag = true;
}
return;
}
if (!theValidFlag )
{
throw new SAXException("NPCGen.Options.InvalidFileFormat"); //$NON-NLS-1$
}
if ( theState == ParserState.INIT )
{
if ( "class".equals(aName) ) //$NON-NLS-1$
{
if ( anAttrs != null )
{
final String classKey = anAttrs.getValue("key"); //$NON-NLS-1$
final PCClass pcClass = Globals.getContext().getReferenceContext().silentlyGetConstructedCDOMObject(PCClass.class, classKey);
if ( pcClass == null )
{
Logging.errorPrintLocalised("Exceptions.PCGenParser.ClassNotFound", classKey); //$NON-NLS-1$
}
else
{
theCurrentData = new ClassData( pcClass );
theState = ParserState.CLASSDATA;
}
}
}
}
else if ( theState == ParserState.CLASSDATA )
{
if ( "stats".equals(aName) ) //$NON-NLS-1$
{
theState = ParserState.STATDATA;
}
else if ( "skills".equals(aName) ) //$NON-NLS-1$
{
theState = ParserState.SKILLDATA;
}
else if ( "abilities".equals(aName) ) //$NON-NLS-1$
{
theState = ParserState.ABILITYDATA;
theCurrentCategory = AbilityCategory.FEAT;
if ( anAttrs != null )
{
final String catName = anAttrs.getValue("category"); //$NON-NLS-1$
if ( catName != null )
{
theCurrentCategory = SettingsHandler.getGame().getAbilityCategory(catName);
}
}
}
else if ( "spells".equals(aName) ) //$NON-NLS-1$
{
theState = ParserState.SPELLDATA;
theCurrentSpellType = SpellType.KNOWN;
if ( anAttrs != null )
{
final String bookName = anAttrs.getValue("type"); //$NON-NLS-1$
if ( bookName != null )
{
if ( "Prepared Spells".equals(bookName) ) //$NON-NLS-1$
{
theCurrentSpellType = SpellType.PREPARED;
}
}
}
}
else if ( "subclasses".equals(aName) ) //$NON-NLS-1$
{
theState = ParserState.SUBCLASSDATA;
}
}
else if ( theState == ParserState.STATDATA )
{
if ( "stat".equals(aName) ) //$NON-NLS-1$
{
if ( anAttrs != null )
{
final int weight = getWeight(anAttrs);
final String statAbbr = anAttrs.getValue("value"); //$NON-NLS-1$
if ( statAbbr != null )
{
PCStat stat = Globals.getContext().getReferenceContext()
.silentlyGetConstructedCDOMObject(PCStat.class, statAbbr);
theCurrentData.addStat(stat, weight);
}
}
}
}
else if ( theState == ParserState.SKILLDATA )
{
if ( "skill".equals(aName) ) //$NON-NLS-1$
{
if ( anAttrs != null )
{
final int weight = getWeight(anAttrs);
final String key = anAttrs.getValue("value"); //$NON-NLS-1$
if ( key != null )
{
if ( "*".equals(key) ) //$NON-NLS-1$
{
remainingWeight = weight;
}
else if (key.startsWith("TYPE")) //$NON-NLS-1$
{
final List<Skill> skillsOfType = Globals.getPObjectsOfType(Globals
.getContext().getReferenceContext().getConstructedCDOMObjects(Skill.class),
key.substring(5));
if (skillsOfType.isEmpty())
{
Logging.debugPrint("NPCGenerator: No skills of type found (" + key + ")"); //$NON-NLS-1$ //$NON-NLS-2$
}
}
else
{
final Skill skill = Globals.getContext().getReferenceContext().silentlyGetConstructedCDOMObject(Skill.class, key);
if (skill == null)
{
Logging.debugPrint("NPCGenerator: Skill not found (" + key + ")"); //$NON-NLS-1$ //$NON-NLS-2$
}
}
if ( weight > 0 && !key.equals("*") ) //$NON-NLS-1$
{
theCurrentData.addSkill(key, weight);
}
else
{
removeList.add(key);
}
}
}
}
}
else if ( theState == ParserState.ABILITYDATA )
{
if ( "ability".equals(aName) ) //$NON-NLS-1$
{
if ( anAttrs != null )
{
final int weight = getWeight(anAttrs);
final String key = anAttrs.getValue("value"); //$NON-NLS-1$
if ( key != null )
{
if ( "*".equals(key) ) //$NON-NLS-1$
{
remainingWeight = weight;
}
else if (key.startsWith("TYPE")) //$NON-NLS-1$
{
Type type = Type.getConstant(key.substring(5));
for (final Ability ability : Globals.getContext().getReferenceContext()
.getManufacturer(Ability.class,
theCurrentCategory).getAllObjects())
{
if (!ability.containsInList(ListKey.TYPE, type))
{
continue;
}
if (ability.getSafe(ObjectKey.VISIBILITY) == Visibility.DEFAULT)
{
if (weight > 0)
{
theCurrentData.addAbility(theCurrentCategory, ability, weight);
}
else
{
// We have to remove any feats of this
// type.
// TODO - This is a little goofy. We
// already have the feat but we will
// store the key and reretrieve it.
removeList.add(ability.getKeyName());
}
}
}
}
else
{
final Ability ability = Globals.getContext().getReferenceContext().silentlyGetConstructedCDOMObject(
Ability.class, theCurrentCategory, key);
if (ability == null)
{
Logging.debugPrint("Ability (" + key + ") not found"); //$NON-NLS-1$ //$NON-NLS-2$
} else
if (weight > 0)
{
theCurrentData.addAbility(theCurrentCategory, ability, weight);
}
else
{
// We have to remove any feats of this
// type.
// TODO - This is a little goofy. We
// already have the feat but we will
// store the key and reretrieve it.
removeList.add(ability.getKeyName());
}
}
}
}
}
}
else if ( theState == ParserState.SPELLDATA )
{
if ( "level".equals(aName) && anAttrs != null ) //$NON-NLS-1$
{
final String lvlStr = anAttrs.getValue("id"); //$NON-NLS-1$
if ( lvlStr != null )
{
theCurrentLevel = Integer.parseInt( lvlStr );
theState = ParserState.SPELLLEVELDATA;
}
}
}
else if ( theState == ParserState.SPELLLEVELDATA )
{
if ( "spell".equals(aName) && anAttrs != null ) //$NON-NLS-1$
{
final int weight = getWeight(anAttrs);
final String key = anAttrs.getValue("name"); //$NON-NLS-1$
if ( key != null )
{
if ( "*".equals(key) ) //$NON-NLS-1$
{
remainingWeight = weight;
}
else if (key.startsWith("SCHOOL")) //$NON-NLS-1$
{
// Not sure how to do this yet
}
else
{
final Spell spell = Globals.getContext().getReferenceContext()
.silentlyGetConstructedCDOMObject(Spell.class, key);
if ( spell != null )
{
if ( theCurrentSpellType == SpellType.KNOWN )
{
theCurrentData.addKnownSpell( theCurrentLevel, spell, weight );
}
else if ( theCurrentSpellType == SpellType.PREPARED )
{
theCurrentData.addPreparedSpell( theCurrentLevel, spell, weight );
}
}
else
{
Logging.errorPrint("Spell \"" + key + "\" not found.");
}
}
}
}
}
else if ( theState == ParserState.SUBCLASSDATA )
{
if ( "subclass".equals(aName) && anAttrs != null ) //$NON-NLS-1$
{
final int weight = getWeight(anAttrs);
final String key = anAttrs.getValue("value"); //$NON-NLS-1$
theCurrentData.addSubClass(key, weight);
}
}
}
/**
* @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
*/
@Override
public void endElement(final String uri, final String localName, final String qName)
{
// If we aren't in a nested state, ignore the end tag as the
// start tag was obviously ignored.
if ( theState == ParserState.INIT )
{
return;
}
if ( "skills".equals(qName) && theState == ParserState.SKILLDATA ) //$NON-NLS-1$
{
if (remainingWeight > 0)
{
// Add all remaining skills at this weight.
for ( final Skill skill : Globals.getContext().getReferenceContext().getConstructedCDOMObjects(Skill.class) )
{
if (skill.getSafe(ObjectKey.VISIBILITY) == Visibility.DEFAULT)
{
theCurrentData.addSkill(skill.getKeyName(), remainingWeight);
}
}
remainingWeight = -1;
}
for ( final String remove : removeList )
{
theCurrentData.removeSkill(remove);
}
removeList = new ArrayList<>();
theState = ParserState.CLASSDATA;
}
else if ( "abilities".equals(qName) && theState == ParserState.ABILITYDATA ) //$NON-NLS-1$
{
if ( remainingWeight > 0 )
{
// Add all abilities at this weight.
for (Ability ability : Globals.getContext().getReferenceContext()
.getManufacturer(Ability.class, theCurrentCategory)
.getAllObjects())
{
if ( ability.getSafe(ObjectKey.VISIBILITY) == Visibility.DEFAULT)
{
theCurrentData.addAbility(theCurrentCategory, ability, remainingWeight);
}
}
remainingWeight = -1;
}
for ( final String remove : removeList )
{
Ability ability = Globals.getContext().getReferenceContext()
.silentlyGetConstructedCDOMObject(Ability.class,
theCurrentCategory, remove);
theCurrentData.removeAbility(theCurrentCategory, ability);
}
removeList = new ArrayList<>();
theCurrentCategory = null;
theState = ParserState.CLASSDATA;
}
else if ( "class".equals(qName) && theState != ParserState.INIT ) //$NON-NLS-1$
{
theList.add(theCurrentData);
theState = ParserState.INIT;
}
else if ( "stats".equals(qName)) //$NON-NLS-1$
{
theState = ParserState.CLASSDATA;
}
else if ( "level".equals(qName)) //$NON-NLS-1$
{
if ( remainingWeight > 0 )
{
// Add all spells at this weight.
final List<Spell> allSpells = getSpellsIn(theCurrentLevel, Collections.singletonList(theCurrentData.getPCClass().get(ObjectKey.CLASS_SPELLLIST)));
for ( final Spell spell : allSpells )
{
if ( theCurrentSpellType == SpellType.KNOWN )
{
theCurrentData.addKnownSpell( theCurrentLevel, spell, remainingWeight );
}
else if ( theCurrentSpellType == SpellType.PREPARED )
{
theCurrentData.addPreparedSpell( theCurrentLevel, spell, remainingWeight );
}
}
remainingWeight = -1;
}
for ( final String remove : removeList )
{
final Spell spell = Globals.getContext().getReferenceContext().silentlyGetConstructedCDOMObject(Spell.class, remove );
if ( theCurrentSpellType == SpellType.KNOWN )
{
theCurrentData.removeKnownSpell(theCurrentLevel, spell);
}
else if ( theCurrentSpellType == SpellType.PREPARED )
{
theCurrentData.removeKnownSpell(theCurrentLevel, spell);
}
}
removeList = new ArrayList<>();
theCurrentLevel = -1;
theState = ParserState.SPELLDATA;
}
else if ( "spells".equals(qName) ) //$NON-NLS-1$
{
theState = ParserState.CLASSDATA;
theCurrentSpellType = SpellType.KNOWN;
}
else if ( "subclasses".equals(qName) ) //$NON-NLS-1$
{
theState = ParserState.CLASSDATA;
}
}
private int getWeight( final Attributes anAttrs )
{
int weight = 1;
final String wtStr = anAttrs.getValue("weight"); //$NON-NLS-1$
if ( wtStr != null )
{
weight = Integer.parseInt(wtStr.trim());
}
return weight;
}
/**
* Returns a List of Spell with following criteria:
*
* @param level (optional, ignored if < 0),
* @param spellLists the lists of spells
* @param pc TODO
* @return a List of Spell
*/
public static List<Spell> getSpellsIn(final int level, List<? extends CDOMList<Spell>> spellLists)
{
MasterListInterface masterLists = SettingsHandler.getGame().getMasterLists();
ArrayList<CDOMReference<CDOMList<Spell>>> useLists = new ArrayList<>();
for (CDOMReference ref : masterLists.getActiveLists())
{
for (CDOMList<Spell> list : spellLists)
{
if (ref.contains(list))
{
useLists.add(ref);
break;
}
}
}
boolean allLevels = level == -1;
Set<Spell> spellList = new HashSet<>();
for (CDOMReference<CDOMList<Spell>> ref : useLists)
{
for (Spell spell : masterLists.getObjects(ref))
{
Collection<AssociatedPrereqObject> assoc = masterLists
.getAssociations(ref, spell);
for (AssociatedPrereqObject apo : assoc)
{
// TODO This null for source is incorrect!
// TODO Not sure if effect of null for PC
if (PrereqHandler.passesAll(apo.getPrerequisiteList(), (PlayerCharacter) null,
null))
{
int lvl = apo
.getAssociation(AssociationKey.SPELL_LEVEL);
if (allLevels || level == lvl)
{
spellList.add(spell);
break;
}
}
}
}
}
return new ArrayList<>(spellList);
}
}