//t
// @(#)XMLVariantParser.java 7/2002
//
// Copyright 2002 Zachary DelProposto. All rights reserved.
// Use is subject to license terms.
//
//
// 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.
//
// This program 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 General Public License for more details.
//
// 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., 675 Mass Ave, Cambridge, MA 02139, USA.
// Or from http://www.gnu.org/
//
package dip.world.variant.parser;
import dip.world.variant.VariantManager;
import dip.world.variant.data.Variant;
import dip.world.variant.data.SupplyCenter;
import dip.world.variant.data.InitialState;
import dip.world.variant.data.MapGraphic;
import dip.world.variant.data.ProvinceData;
import dip.world.variant.data.BorderData;
import dip.world.Phase;
import dip.world.Power;
import dip.world.Unit;
import dip.world.Coast;
import dip.gui.map.DefaultMapRenderer2;
import dip.gui.map.SVGUtils;
import dip.misc.LRUCache;
import dip.misc.Utils;
import dip.misc.Log;
import java.io.*;
import java.net.*;
import java.util.*;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.w3c.dom.*;
import org.w3c.dom.svg.SVGElement;
/**
* Parses an XML Variant description.
*
*/
public class XMLVariantParser implements VariantParser
{
// XML Element constants
public static final String EL_VARIANTS = "VARIANTS";
public static final String EL_VARIANT = "VARIANT";
public static final String EL_DESCRIPTION = "DESCRIPTION";
public static final String EL_MAP = "MAP";
public static final String EL_STARTINGTIME = "STARTINGTIME";
public static final String EL_INITIALSTATE = "INITIALSTATE";
public static final String EL_SUPPLYCENTER = "SUPPLYCENTER";
public static final String EL_POWER = "POWER";
public static final String EL_MAP_DEFINITION = "MAP_DEFINITION";
public static final String EL_MAP_GRAPHIC = "MAP_GRAPHIC";
public static final String EL_VICTORYCONDITIONS = "VICTORYCONDITIONS";
public static final String EL_GAME_LENGTH = "GAME_LENGTH";
public static final String EL_YEARS_WITHOUT_SC_CAPTURE = "YEARS_WITHOUT_SC_CAPTURE";
public static final String EL_WINNING_SUPPLY_CENTERS = "WINNING_SUPPLY_CENTERS";
public static final String EL_RULEOPTIONS = "RULEOPTIONS";
public static final String EL_RULEOPTION = "RULEOPTION";
// XML Attribute constants
public static final String ATT_ALIASES = "aliases";
public static final String ATT_VERSION = "version";
public static final String ATT_URI = "URI";
public static final String ATT_DEFAULT = "default";
public static final String ATT_TITLE = "title";
public static final String ATT_DESCRIPTION = "description";
public static final String ATT_THUMBURI = "thumbURI";
public static final String ATT_ADJACENCYURI = "adjacencyURI";
public static final String ATT_NAME = "name";
public static final String ATT_ACTIVE = "active";
public static final String ATT_ADJECTIVE = "adjective";
public static final String ATT_ALTNAMES = "altnames";
public static final String ATT_TURN = "turn";
public static final String ATT_VALUE = "value";
public static final String ATT_PROVINCE = "province";
public static final String ATT_HOMEPOWER = "homepower";
public static final String ATT_OWNER = "owner";
public static final String ATT_POWER = "power";
public static final String ATT_UNIT = "unit";
public static final String ATT_UNITCOAST = "unitcoast";
public static final String ATT_ALLOW_BC_YEARS = "allowBCYears";
public static final String ATT_PREFERRED_UNIT_STYLE = "preferredUnitStyle";
public static final String ATT_ID = "id";
public static final String ATT_REF = "ref";
// il8n error message constants
private static final String ERR_NO_ELEMENT = "XMLVariantParser.noelement";
// instance variables
private Document doc = null;
private DocumentBuilder docBuilder = null;
private List variantList = null;
private XMLProvinceParser provinceParser = null;
/** Create an XMLVariantParser */
/*
public XMLVariantParser(boolean isValidating)
throws ParserConfigurationException
{
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setValidating(isValidating);
dbf.setCoalescing(false);
dbf.setIgnoringComments(true);
docBuilder = dbf.newDocumentBuilder();
docBuilder.setErrorHandler(new XMLErrorHandler());
provinceParser = new XMLProvinceParser(dbf);
variantList = new LinkedList();
AdjCache.init(provinceParser);
}// XMLVariantParser()
*/
/** Create an XMLVariantParser */
public XMLVariantParser(final DocumentBuilderFactory dbf)
throws ParserConfigurationException
{
docBuilder = dbf.newDocumentBuilder();
docBuilder.setErrorHandler(new XMLErrorHandler());
FastEntityResolver.attach(docBuilder);
provinceParser = new XMLProvinceParser(dbf);
variantList = new LinkedList();
AdjCache.init(provinceParser);
}// XMLVariantParser()
/** Parse the given input stream; parsed data available via <code>getVariants()</code>
<p>
Note that when this method is called, any previous Variants (if any exist) are
cleared.
*/
public void parse(InputStream is, URL variantPackageURL)
throws IOException, SAXException
{
Log.println("XMLVariantParser: Parsing: ", variantPackageURL);
long time = System.currentTimeMillis();
// cleanup cache (very important to remove references!)
AdjCache.clear();
variantList.clear();
if(variantPackageURL == null)
{
throw new IllegalArgumentException();
}
AdjCache.setVariantPackageURL(variantPackageURL);
doc = docBuilder.parse(is);
procVariants();
Log.printTimed(time, " time: ");
}// parse()
/** Cleanup, clearing any references/resources */
public void close()
{
AdjCache.clear();
variantList.clear();
}// close()
/** Returns an array of Variant objects.
<p>
Will never return null. Note that parse() must be called before
this will return any information.
*/
public Variant[] getVariants()
{
return (Variant[]) variantList.toArray(new Variant[variantList.size()]);
}// getVariants()
/** Process the Variant list description file */
private void procVariants()
throws IOException, SAXException
{
// setup map definition ID hashmap
HashMap mapDefTable = new HashMap(7); // maps String ID -> MapDef
// find the root element (VARIANTS), and all VARIANT elements underneath.
Element root = doc.getDocumentElement();
// get map definitions (at least one, under VARIANT)
NodeList mapDefEls = root.getElementsByTagName(EL_MAP_DEFINITION);
for(int i=0; i<mapDefEls.getLength(); i++)
{
Element elMapDef = (Element) mapDefEls.item(i);
// get description
String description = null;
Element element = getSingleElementByName(elMapDef, EL_DESCRIPTION);
if(element != null)
{
Node text = element.getFirstChild();
description = text.getNodeValue();
}
// create MapDef
MapDef md = new MapDef(
elMapDef.getAttribute(ATT_ID),
elMapDef.getAttribute(ATT_TITLE),
elMapDef.getAttribute(ATT_URI),
elMapDef.getAttribute(ATT_THUMBURI),
elMapDef.getAttribute(ATT_PREFERRED_UNIT_STYLE),
description );
// if no title, error!
if("".equals(md.getTitle()))
{
throw new IOException("map id="+md.getID()+" missing a title (name)");
}
// map it.
mapDefTable.put(md.getID(), md);
}
// search for variant data
NodeList variantElements = root.getElementsByTagName(EL_VARIANT);
for(int i=0; i<variantElements.getLength(); i++)
{
Variant variant = new Variant();
Element elVariant = (Element) variantElements.item(i);
// VARIANT attributes
variant.setName( elVariant.getAttribute(ATT_NAME) );
variant.setDefault( Boolean.valueOf(elVariant.getAttribute(ATT_DEFAULT)).booleanValue() );
variant.setVersion( parseFloat(elVariant.getAttribute(ATT_VERSION)) );
variant.setAliases( Utils.parseCSV(elVariant.getAttribute(ATT_ALIASES)) );
// description
Element element = getSingleElementByName(elVariant, EL_DESCRIPTION);
checkElement(element, EL_DESCRIPTION);
Node text = element.getFirstChild();
variant.setDescription( text.getNodeValue() );
// starting time
element = getSingleElementByName(elVariant, EL_STARTINGTIME);
checkElement(element, EL_STARTINGTIME);
variant.setStartingPhase( Phase.parse(element.getAttribute(ATT_TURN)) );
variant.setBCYearsAllowed( Boolean.valueOf(element.getAttribute(ATT_ALLOW_BC_YEARS)).booleanValue() );
// if start is BC, and BC years are not allowed, then BC years ARE allowed.
if(variant.getStartingPhase().getYear() < 0)
{
variant.setBCYearsAllowed(true);
}
// victory conditions (single, with single subitems)
element = getSingleElementByName(elVariant, EL_VICTORYCONDITIONS);
checkElement(element, EL_VICTORYCONDITIONS);
Element vcSubElement = getSingleElementByName(element, EL_WINNING_SUPPLY_CENTERS);
if(vcSubElement != null)
{
variant.setNumSCForVictory( parseInt(vcSubElement.getAttribute(ATT_VALUE)) );
}
vcSubElement = getSingleElementByName(element, EL_YEARS_WITHOUT_SC_CAPTURE);
if(vcSubElement != null)
{
variant.setMaxYearsNoSCChange( parseInt(vcSubElement.getAttribute(ATT_VALUE)) );
}
vcSubElement = getSingleElementByName(element, EL_GAME_LENGTH);
if(vcSubElement != null)
{
variant.setMaxGameTimeYears( parseInt(vcSubElement.getAttribute(ATT_VALUE)) );
}
// powers (multiple)
NodeList nodes = elVariant.getElementsByTagName(EL_POWER);
final int nodeListLen = nodes.getLength();
List powerList = new ArrayList(nodeListLen);
for(int j=0; j<nodeListLen; j++)
{
element = (Element) nodes.item(j);
String name = element.getAttribute(ATT_NAME);
final boolean isActive = Boolean.valueOf( element.getAttribute(ATT_ACTIVE) ).booleanValue();
String adjective = element.getAttribute(ATT_ADJECTIVE);
String[] altNames = Utils.parseCSVXE( element.getAttribute(ATT_ALTNAMES) );
String[] names = new String[altNames.length + 1];
names[0] = name;
System.arraycopy(altNames, 0, names, 1, altNames.length);
Power power = new Power(names, adjective, isActive);
powerList.add(power);
}
variant.setPowers(powerList);
// supply centers (multiple)
nodes = elVariant.getElementsByTagName(EL_SUPPLYCENTER);
List supplyCenterList = new ArrayList(nodes.getLength());
for(int j=0; j<nodes.getLength(); j++)
{
element = (Element) nodes.item(j);
SupplyCenter supplyCenter = new SupplyCenter();
supplyCenter.setProvinceName( element.getAttribute(ATT_PROVINCE) );
supplyCenter.setHomePowerName( element.getAttribute(ATT_HOMEPOWER) );
supplyCenter.setOwnerName( element.getAttribute(ATT_OWNER) );
supplyCenterList.add(supplyCenter);
}
variant.setSupplyCenters(supplyCenterList);
// //paths
// nodes = SVGUtils.findNodeWithID(elVariant, "MouseLayer").getChildNodes();
// List<String[]> pathList = new ArrayList<String[]>(nodes.getLength());
// for (int j = 0; j < nodes.getLength(); j++) {
// element = (Element) nodes.item(j);
// if (element.getNodeName().equals("path")){
// pathList.add(new String[]{element.getAttribute("d")});
// }else if(element.getNodeName().equals("g")){
// String[] s = new String[element.getChildNodes().getLength()];
// for (int j2 = 0; j2 < element.getChildNodes().getLength(); j2++) {
// s[j2] = ((Element) element.getChildNodes().item(j2)).getAttribute("g");
// }
// pathList.add(s);
// }
// }
// initial state (multiple)
nodes = elVariant.getElementsByTagName(EL_INITIALSTATE);
List stateList = new ArrayList(nodes.getLength());
for(int j=0; j<nodes.getLength(); j++)
{
element = (Element) nodes.item(j);
InitialState initialState = new InitialState();
initialState.setProvinceName( element.getAttribute(ATT_PROVINCE) );
initialState.setPowerName( element.getAttribute(ATT_POWER) );
initialState.setUnitType( Unit.Type.parse(element.getAttribute(ATT_UNIT)) );
initialState.setCoast( Coast.parse(element.getAttribute(ATT_UNITCOAST)) );
stateList.add(initialState);
}
variant.setInitialStates(stateList);
// MAP element and children
element = getSingleElementByName(elVariant, EL_MAP);
// MAP adjacency URI; process it using ProvinceData parser
try
{
URI adjacencyURI = new URI(element.getAttribute(ATT_ADJACENCYURI));
variant.setProvinceData( AdjCache.getProvinceData(adjacencyURI) );
variant.setBorderData( AdjCache.getBorderData(adjacencyURI) );
}
catch(URISyntaxException e)
{
throw new IOException(e.getMessage());
}
// MAP_GRAPHIC element (multiple)
nodes = element.getElementsByTagName(EL_MAP_GRAPHIC);
List graphicList = new ArrayList(nodes.getLength());
for(int j=0; j<nodes.getLength(); j++)
{
Element mgElement = (Element) nodes.item(j);
final String refID = mgElement.getAttribute(ATT_REF);
final boolean isDefault = Boolean.valueOf(mgElement.getAttribute(ATT_DEFAULT)).booleanValue();
final String preferredUnitStyle = mgElement.getAttribute(ATT_PREFERRED_UNIT_STYLE);
// lookup; if we didn't find it, throw an exception
MapDef md = (MapDef) mapDefTable.get(refID);
if(md == null)
{
throw new IOException("MAP_GRAPHIC refers to unknown ID: \""+refID+"\"");
}
// create the MapGraphic object
MapGraphic mapGraphic = new MapGraphic(
md.getMapURI(),
isDefault,
md.getTitle(),
md.getDescription(),
md.getThumbURI(),
("".equals(preferredUnitStyle)) ? md.getPrefUnitStyle() : preferredUnitStyle );
graphicList.add(mapGraphic);
}
variant.setMapGraphics( graphicList );
// rule options (if any have been set)
// this element is optional.
element = getSingleElementByName(elVariant, EL_RULEOPTIONS);
if(element != null)
{
nodes = element.getElementsByTagName(EL_RULEOPTION);
List ruleNVPList = new ArrayList(nodes.getLength());
for(int j=0; j<nodes.getLength(); j++)
{
Element rElement = (Element) nodes.item(j);
Variant.NameValuePair nvp = new Variant.NameValuePair(
rElement.getAttribute(ATT_NAME),
rElement.getAttribute(ATT_VALUE)
);
ruleNVPList.add(nvp);
}
variant.setRuleOptionNVPs( ruleNVPList );
}
else
{
variant.setRuleOptionNVPs( new ArrayList(0) );
}
// add variant to list of variants
variantList.add(variant);
}// for(i)
}// procVariants()
/** Checks that an element is present */
private void checkElement(Element element, String name)
throws SAXException
{
if(element == null)
{
throw new SAXException(Utils.getLocalString(ERR_NO_ELEMENT, name));
}
}// checkElement()
/** Get an Element by name; only returns a single element. */
private Element getSingleElementByName(Element parent, String name)
{
NodeList nodes = parent.getElementsByTagName(name);
return (Element) nodes.item(0);
}// getSingleElementByName()
/** Integer parser; throws an exception if number cannot be parsed. */
private int parseInt(String value)
throws IOException
{
String message = "";
try
{
return Integer.parseInt(value);
}
catch(NumberFormatException e)
{
message = e.toString();
}
throw new IOException(message);
}// parseInt()
/** Float parser; throws an exception if number cannot be parsed. Value must be >= 0.0 */
private float parseFloat(String value)
throws IOException
{
String message = "";
try
{
final float floatValue = Float.parseFloat(value);
if(floatValue < 0.0f)
{
throw new NumberFormatException("Value must be >= 0");
}
return floatValue;
}
catch(NumberFormatException e)
{
message = e.toString();
}
throw new IOException(message);
}// parseInt()
/**
* Inner class which caches XML adjacency data (ProvinceData and BorderData),
* which may be shared between different variants (if the variants use the
* same adjacency data).
* <p>
* NOTE: this depends on the XMLVariantParser variable "adjCache", since inner classes
* cannot have statics (unless they inner class is static, which just creates more problems;
* this is a simpler solution)
*
*/
private static class AdjCache
{
private static URL vpURL = null;
private static XMLProvinceParser pp = null;
private static LRUCache adjCache = null; // URI -> AdjCache objects
// instance variables
private ProvinceData[] provinceData;
private BorderData[] borderData;
public AdjCache()
{
}// AdjCache()
/** initialization */
public static void init(XMLProvinceParser provinceParser)
{
pp = provinceParser;
adjCache = new LRUCache(6);
}// AdjCache()
/** Sets the variant package URL */
public static void setVariantPackageURL(URL variantPackageURL)
{
vpURL = variantPackageURL;
}// setVariantPackageURL()
/** Clears the cache. */
public static void clear()
{
adjCache.clear();
}// clear()
/** Gets the ProvinceData for a given adjacency URI */
public static ProvinceData[] getProvinceData(URI adjacencyURI)
throws IOException, SAXException
{
AdjCache ac = get(adjacencyURI);
return ac.provinceData;
}// getProvinceData()
/** Gets the BorderData for a given adjacency URI */
public static BorderData[] getBorderData(URI adjacencyURI)
throws IOException, SAXException
{
AdjCache ac = get(adjacencyURI);
return ac.borderData;
}// getBorderData()
/** Gets the AdjCache object from the cache, or parses from the URI, as appropriate */
private static AdjCache get(URI adjacencyURI)
throws IOException, SAXException
{
// see if we already have the URI data cached.
if(adjCache.get(adjacencyURI) != null)
{
//Log.println(" AdjCache: using cached adjacency data: ", adjacencyURI);
return (AdjCache) adjCache.get(adjacencyURI);
}
// it's not cached. resolve URI.
URL url = VariantManager.getResource(vpURL, adjacencyURI);
if(url == null)
{
throw new IOException("Could not convert URI: "+adjacencyURI+" from variant package: "+vpURL);
}
// parse resolved URI
//Log.println(" AdjCache: not in cache: ", adjacencyURI);
InputStream is = null;
try
{
is = new BufferedInputStream(url.openStream());
pp.parse(is);
}
finally
{
if(is != null)
{
try { is.close(); } catch (IOException e) {}
}
}
// cache and return parsed data.
AdjCache ac = new AdjCache();
ac.provinceData = pp.getProvinceData();
ac.borderData = pp.getBorderData();
adjCache.put(adjacencyURI, ac);
return ac;
}// get()
}// inner class AdjCache
/**
* Class that holds MAP_DEFINITION data, which is
* inserted into a hashtable for later recall.
*/
private class MapDef
{
private final String id;
private final String title;
private final String mapURI;
private final String thumbURI;
private final String preferredUnitStyle;
private final String description;
public MapDef(String id, String title, String mapURI, String thumbURI,
String preferredUnitStyle, String description)
{
this.id = id;
this.title = title;
this.mapURI = mapURI;
this.thumbURI = thumbURI;
this.preferredUnitStyle = preferredUnitStyle;
this.description = description;
}// MapDef()
public String getID() { return id; }
public String getTitle() { return title; }
public String getMapURI() { return mapURI; }
public String getThumbURI() { return thumbURI; }
public String getPrefUnitStyle() { return preferredUnitStyle; }
public String getDescription() { return description; }
}// inner class MapDef
}// class XMLVariantParser