+//
+// Copyright (c) 2013 Motoyuki Kasahara
+//
+// 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, see <http://www.gnu.org/licenses/>.
+//
+package jp.sourceforge.gamerandomizerlib;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Locale;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+/**
+ * <p>
+ * Concrete class of 'GameBuilder'.
+ * </p>
+ *
+ * <p>
+ * Its object builds 'Game' object by reading XML files which define game
+ * data.
+ * </p>
+ *
+ * <p>
+ * For each game, it is required to prepare some XML files, a file
+ * '__game.xml' (starting with two '_') and some '<dfn>expansion</dfn>.xml'
+ * files. '__game.xml' defines basic information of the game, such as a
+ * title of the game. '<dfn>expansion</dfn>.xml' defines an expansion of
+ * the game and cards provided by the expansion.
+ * </p>
+ *
+ * <p>
+ * Those files must be placed at the same directory, and its directory must
+ * be '<dfn>game</dfn>/<dfn>language</dfn>_<dfn>country</dfn>'
+ * (e.g. 'dominon/en_US') or '<dfn>game</dfn>/<dfn>language</dfn>'
+ * (e.g. 'dominion/en'). <dfn>game</dfn> is ID of the game.
+ * '<dfn>language</dfn>' and '<dfn>country</dfn>' are values returned from
+ * getLanguage() and getCountry() methods of 'java.util.Locale' class.
+ * According with locale setting, an object of this class reads XML files
+ * at a corresponding directory.
+ * </p>
+ *
+ * <p>
+ * The following is an example of file hierarchy. They define two games
+ * 'dominon' and 'ascension'. They provide data for 'English - United States'
+ * and 'Japanese' but Japanese data lacks definition of the game 'ascension'
+ * and the expansion 'intrigue' of 'dominion'.
+ * </p>
+ *
+ * <pre><code>
+ * games/
+ * + dominion/
+ * + en_US/
+ * + __game.xml
+ * + dominion.xml
+ * + intrigue.xml
+ * + ja/
+ * + __game.xml
+ * + dominion.xml
+ * + ascension/
+ * + en_US/
+ * + __game.xml
+ * + ascension.xml
+ * </code></pre>
+ *
+ * <p>
+ * To build 'Game' object, specify the path to 'game/' directory as an
+ * argument to constructor of this class, like:
+ * </p>
+ *
+ * <pre><code>
+ * GameBuilder builder = new GameBuilderXMLFile("games");
+ * List<Game> gameList = builder.buildAll();
+ * </code></pre>
+ *
+ * @author Motoyuki Kasahara
+ * @version 1.0
+ */
+public class GameBuilderXMLFile extends GameBuilder {
+ //
+ // Expected attribute values.
+ //
+ class Expectancy {
+ String gameId;
+ String expansionId;
+ String language;
+ String country;
+
+ Expectancy() {
+ gameId = "";
+ expansionId = "";
+ language = "";
+ country = "";
+ }
+ }
+
+ // Path to a top directory of XML files.
+ private File directoryFile;
+
+ /**
+ * Constructs an object.
+ *
+ * It sets the top directory of XML files to '.' (the current directory)
+ * and sets locale to the current value of the default locale.
+ */
+ public GameBuilderXMLFile() {
+ super();
+ directoryFile = new File(".");
+ }
+
+ /**
+ * Constructs an object.
+ *
+ * It sets the top directory of XML files to the specified path and
+ * sets locale to the current value of the default locale.
+ *
+ * @param dir path to the top directory
+ */
+ public GameBuilderXMLFile(File dir) {
+ super();
+
+ assert dir != null;
+ directoryFile = dir;
+ }
+
+ /**
+ * Constructs an object.
+ *
+ * It sets the top directory of XML files to the specified path and
+ * sets locale to the current value of the default locale.
+ *
+ * @param dir path to the top directory
+ */
+ public GameBuilderXMLFile(String dir) {
+ this(new File(dir));
+ }
+
+ /**
+ * Constructs an object.
+ *
+ * It sets the top directory of XML files and locale to the specified
+ * values.
+ *
+ * @param dir path to the top directory
+ * @param localeArg a locale
+ */
+ public GameBuilderXMLFile(File dir, Locale localeArg) {
+ super(localeArg);
+
+ assert dir != null;
+ directoryFile = dir;
+ }
+
+ /**
+ * Constructs an object.
+ *
+ * It sets the top directory of XML files and locale to the specified
+ * values.
+ *
+ * @param dir path to the top directory
+ * @param localeArg a locale
+ */
+ public GameBuilderXMLFile(String dir, Locale localeArg) {
+ this(new File(dir), localeArg);
+ }
+
+ @Override
+ public Game build(String id)
+ throws GameBuilderException {
+ Expectancy exp = new Expectancy();
+ File file = findGameXMLFile(id, exp);
+ if (file == null)
+ return null;
+ Game g = readGameXMLFile(file, exp);
+ readExpansionXMLFiles(g, exp);
+ return g;
+ }
+
+ @Override
+ public Game build(Game game)
+ throws GameBuilderException {
+ Expectancy exp = new Expectancy();
+ File file = findGameXMLFile(game.getId(), exp);
+ if (file == null)
+ return game;
+ readExpansionXMLFiles(game, exp);
+ return game;
+ }
+
+ @Override
+ public List<Game> buildAll(List<Game> gameList)
+ throws GameBuilderException {
+ assert gameList != null;
+
+ int initListSize = gameList.size();
+ readXMLFiles(gameList);
+
+ ListIterator<Game> it = gameList.listIterator(initListSize);
+ while (it.hasNext()) {
+ Game g = it.next();
+ if (g.getCardsSize() == 0)
+ it.remove();
+ }
+
+ return gameList;
+ }
+
+ //
+ // XML filename.
+ //
+ private static final String XML_FILE_NAME = "__game.xml";
+
+ //
+ // XML format versions this program supports.
+ //
+ private static final String[] SUPPORTED_XML_FORMAT_VERSIONS = {"1"};
+
+ //
+ // Internal method for buildAll().
+ //
+ // It reads all '__game.xml' files under 'directoryFile', parses the
+ // data and puts the data about games into 'gameList'.
+ //
+ private void readXMLFiles(List<Game> gameList)
+ throws GameBuilderException {
+ File[] subdirs = directoryFile.listFiles();
+ if (subdirs == null)
+ return;
+
+ for (File subdir : subdirs) {
+ Expectancy exp = new Expectancy();
+ String subdirName = subdir.getName();
+ File file = findGameXMLFile(subdirName, exp);
+ if (file == null)
+ continue;
+ if (gameList.indexOf(new Game(subdirName, "", 0)) >= 0)
+ continue;
+ Game game = readGameXMLFile(file, exp);
+ readExpansionXMLFiles(game, exp);
+ gameList.add(game);
+ }
+ }
+
+ //
+ // Internal method for build() and buildAll().
+ //
+ // It searches 'subDirName' in 'directoryFile' for '__game.xml' file.
+ // If found, this method returns 'File' object of the XML file.
+ // Otherwise, it returns null.
+ //
+ private File findGameXMLFile(String gameId, Expectancy exp) {
+ String language = locale.getLanguage();
+ String country = locale.getCountry();
+ char sep = File.separatorChar;
+
+ File file1 = new File(String.format("%s%c%s%c%s_%s%c%s",
+ directoryFile.getAbsolutePath(), sep, gameId, sep,
+ language, country, sep, XML_FILE_NAME));
+ if (file1.isFile()) {
+ exp.gameId = gameId;
+ exp.language = language;
+ exp.country = country;
+ return file1;
+ }
+
+ File file2 = new File(String.format("%s%c%s%c%s%c%s",
+ directoryFile.getAbsolutePath(), sep, gameId, sep,
+ language, sep, XML_FILE_NAME));
+ if (file2.isFile()) {
+ exp.gameId = gameId;
+ exp.language = language;
+ exp.country = "";
+ return file2;
+ }
+
+ return null;
+ }
+
+ //
+ // Invalid price value (used for an initial value of price variable.)
+ //
+ private static final int INVALID_SELECTION_SIZE = -1;
+
+ //
+ // Internal method for build() and buildAll().
+ //
+ // It reads game data from 'file', parses the data as XML, and then puts
+ // the data about a game into 'gameList'.
+ //
+ private Game readGameXMLFile(File file, Expectancy exp)
+ throws GameBuilderException {
+ //
+ // Open a file and create an XML parser.
+ //
+ FileReader reader = null;
+ XmlPullParser parser;
+ Game game = null;
+ String fileTitle = getPartialPath(file, 2);
+ try {
+ reader = new FileReader(file);
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ factory.setNamespaceAware(false);
+ parser = factory.newPullParser();
+ parser.setInput(reader);
+ game = readGameXMLData(parser, fileTitle, exp);
+ } catch (GameBuilderXMLFileException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new GameBuilderXMLFileException(e.getCause(), fileTitle,
+ "%s", e.getMessage());
+ } finally {
+ try {
+ if (reader != null)
+ reader.close();
+ } catch (IOException e) {
+ ; // do nothing;
+ }
+ }
+
+ return game;
+ }
+
+ //
+ // Internal method for build() and buildAll().
+ //
+ // It parses XML data, and then returns the data as a 'Game' object.
+ //
+ private Game readGameXMLData(XmlPullParser parser, String fileTitle,
+ Expectancy exp)
+ throws GameBuilderException {
+ //
+ // Get the first event from the XML parser.
+ //
+ Game game = null;
+ String tag = "";
+ String prevTag = "";
+ String id = null;
+ String title = null;
+ int selectionSize = INVALID_SELECTION_SIZE;
+
+ int event;
+ try {
+ event = parser.getEventType();
+ } catch (Exception e) {
+ throw new GameBuilderXMLFileException(e.getCause(), fileTitle,
+ "failed to parse XML, %s", e.getMessage());
+ }
+
+ //
+ // Inteprets the event.
+ //
+ while (event != XmlPullParser.END_DOCUMENT) {
+ int lineNo = parser.getLineNumber();
+ if (event == XmlPullParser.START_TAG) {
+ tag = parser.getName();
+ if (prevTag.equals("")) {
+ if (!tag.equals("game")) {
+ throw new GameBuilderXMLFileException(fileTitle,
+ lineNo, "<game> expected, but got <%s>", tag);
+ }
+ validateGameStartTag(parser, fileTitle, lineNo, exp);
+ id = getAttribute(parser, "id");
+ } else if (prevTag.equals("game")) {
+ if (!tag.equals("game_title")) {
+ throw new GameBuilderXMLFileException(fileTitle,
+ lineNo, "<game_title> expected, but got <%s>",
+ tag);
+ }
+ } else if (prevTag.equals("/game_title")) {
+ if (!tag.equals("selection_size")) {
+ throw new GameBuilderXMLFileException(fileTitle,
+ lineNo, "<selection_size> expected, but got <%s>",
+ tag);
+ }
+ } else {
+ throw new GameBuilderXMLFileException(fileTitle,
+ lineNo, "unknown tag <%s>", tag);
+ }
+ prevTag = tag;
+ } else if (event == XmlPullParser.END_TAG) {
+ tag = "/" + parser.getName();
+ if (tag.equals("/game")) {
+ assert id != null;
+ assert title != null;
+ assert selectionSize > 0;
+ game = new Game(new Game(id, title, selectionSize));
+ }
+ prevTag = tag;
+ } else if (event == XmlPullParser.TEXT) {
+ String text = parser.getText();
+ if (tag.equals("game_title"))
+ title = text;
+ else if (tag.equals("selection_size")) {
+ try {
+ selectionSize = Integer.parseInt(text);
+ } catch (NumberFormatException e) {
+ throw new GameBuilderXMLFileException(fileTitle,
+ lineNo, "not an integer for <selection_size>");
+ }
+ if (selectionSize <= 0) {
+ throw new GameBuilderXMLFileException(fileTitle,
+ lineNo, "<selection_size> is not greater than 0");
+ }
+ }
+ }
+
+ //
+ // Get the next event from the XML parser.
+ //
+ try {
+ event = parser.next();
+ } catch (Exception e) {
+ throw new GameBuilderXMLFileException(e.getCause(), fileTitle,
+ "failed to parse XML, %s", e.getMessage());
+ }
+ }
+
+ if (!tag.equals("/game")) {
+ throw new GameBuilderXMLFileException(fileTitle,
+ parser.getLineNumber(), "unexpected eof");
+ }
+
+ return game;
+ }
+
+ //
+ // Validate attributes of a <game> tag.
+ //
+ private void validateGameStartTag(XmlPullParser parser,
+ String fileTitle, int lineNo, Expectancy exp)
+ throws GameBuilderException {
+ //
+ // Validate 'id'.
+ //
+ String id = getAttribute(parser, "id");
+ if (id == null) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "no 'id' attribute in <game>");
+ }
+ if (!id.equals(exp.gameId)) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "'id' is different from a directory name");
+ }
+
+ //
+ // Validate 'format_version'
+ //
+ String version = getAttribute(parser, "format_version");
+ if (version == null) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "no 'format_version' attribute in <game>");
+ }
+ boolean is_supported_version = false;
+ for (String v : SUPPORTED_XML_FORMAT_VERSIONS) {
+ if (v.equals(version)) {
+ is_supported_version = true;
+ break;
+ }
+ }
+ if (!is_supported_version) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "format_version '%s' not supported", version);
+ }
+
+ //
+ // Validate 'language'.
+ //
+ String language = getAttribute(parser, "language");
+ if (language == null) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "no 'language' attribute in <game>");
+ }
+ if (!language.equals(exp.language)) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "'language' is different from a file name");
+ }
+
+ //
+ // Validate 'country'.
+ //
+ String country = getAttribute(parser, "country");
+ if (country == null) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "no 'country' attribute in <game>");
+ }
+ if (!country.equals(exp.country)) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "'country' is different from a file name");
+ }
+ }
+
+ //
+ // Internal method for build() and buildAll().
+ //
+ // It reads all expansion-definition XML files at 'directoryFile',
+ // parses the data and puts the data about expansions and cards into
+ // 'component'.
+ //
+ private void readExpansionXMLFiles(Game game, Expectancy gameExp)
+ throws GameBuilderException {
+ //
+ // Get expected data values.
+ //
+ char sep = File.separatorChar;
+
+ //
+ // Check if a sub-directory with the locale name exists.
+ //
+ File dir;
+ if (gameExp.country.isEmpty()) {
+ dir = new File(String.format("%s%c%s%c%s",
+ directoryFile.getAbsolutePath(), sep, gameExp.gameId, sep,
+ gameExp.language));
+ } else {
+ dir = new File(String.format("%s%c%s%c%s_%s",
+ directoryFile.getAbsolutePath(), sep, gameExp.gameId, sep,
+ gameExp.language, gameExp.country));
+ }
+ if (!dir.isDirectory())
+ return;
+
+ //
+ // Read all XML files under the locale sub-directory.
+ //
+ File[] files = dir.listFiles();
+ if (files == null)
+ return;
+ for (File file : files) {
+ if (!file.isFile())
+ continue;
+ String fileName = file.getName();
+ if (fileName.startsWith("_") || !fileName.endsWith(".xml"))
+ continue;
+ Expectancy exp = new Expectancy();
+ exp.gameId = gameExp.gameId;
+ exp.expansionId = fileName.substring(0, fileName.length() - 4);
+ exp.language = gameExp.language;
+ exp.country = gameExp.country;
+ readExpansionXMLFile(game, file, exp);
+ }
+ }
+
+ //
+ // Internal method for build() and buildAll().
+ //
+ // It reads data from 'file', parses the data as XML, and then puts
+ // the data about expansions and cards into 'game'.
+ //
+ private void readExpansionXMLFile(Game game, File file, Expectancy exp)
+ throws GameBuilderException {
+ //
+ // Open a file and create an XML parser.
+ //
+ FileReader reader = null;
+ XmlPullParser parser;
+ String fileTitle = getPartialPath(file, 2);
+ try {
+ reader = new FileReader(file);
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ factory.setNamespaceAware(false);
+ parser = factory.newPullParser();
+ parser.setInput(reader);
+ readExpansionXMLData(game, parser, fileTitle, exp);
+ } catch (GameBuilderXMLFileException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new GameBuilderXMLFileException(e.getCause(), fileTitle,
+ "%s", e.getMessage());
+ } finally {
+ try {
+ if (reader != null)
+ reader.close();
+ } catch (IOException e) {
+ ; // do nothing;
+ }
+ }
+ }
+
+ //
+ // Internal method for build() and buildAll().
+ //
+ // It parses XML data, and then puts the data into 'game'.
+ //
+ private void readExpansionXMLData(Game game, XmlPullParser parser,
+ String fileTitle, Expectancy exp)
+ throws GameBuilderException {
+ //
+ // Get the first event from the XML parser.
+ //
+ Expansion expansion = null;
+ String tag = "";
+ String prevTag = "";
+ String expansionId = null;
+ String expansionTitle = null;
+ String cardId = null;
+ String cardTitle = null;
+ String cardPrice = null;
+ int preference = Integer.MAX_VALUE;
+ boolean expansionAdded = false;
+
+ int event;
+ try {
+ event = parser.getEventType();
+ } catch (Exception e) {
+ throw new GameBuilderXMLFileException(e.getCause(), fileTitle,
+ "failed to parse XML, %s", e.getMessage());
+ }
+
+ //
+ // Inteprets the event.
+ //
+ while (event != XmlPullParser.END_DOCUMENT) {
+ int lineNo = parser.getLineNumber();
+ if (event == XmlPullParser.START_TAG) {
+ tag = parser.getName();
+ if (prevTag.equals("")) {
+ if (!tag.equals("expansion")) {
+ throw new GameBuilderXMLFileException(fileTitle,
+ lineNo, "<expansion> expected, but got <%s>", tag);
+ }
+ validateExpansionStartTag(parser, fileTitle, lineNo, exp);
+ expansionId = getAttribute(parser, "id");
+ } else if (prevTag.equals("expansion")) {
+ if (!tag.equals("preference")) {
+ throw new GameBuilderXMLFileException(fileTitle,
+ lineNo, "<preference> expected, but got <%s>",
+ tag);
+ }
+ } else if (prevTag.equals("/preference")) {
+ if (!tag.equals("expansion_title")) {
+ throw new GameBuilderXMLFileException(fileTitle,
+ lineNo, "<expansion_title> expected, but got <%s>",
+ tag);
+ }
+ } else if (prevTag.equals("/expansion_title")
+ || prevTag.equals("/card")) {
+ if (!tag.equals("card")) {
+ throw new GameBuilderXMLFileException(fileTitle,
+ lineNo, "<card> expected, but got <%s>", tag);
+ }
+ cardId = getAttribute(parser, "id");
+ if (cardId == null) {
+ throw new GameBuilderXMLFileException(fileTitle,
+ lineNo, "no 'id' attribute in <expansion>");
+ }
+ } else if (prevTag.equals("card")) {
+ if (!tag.equals("title")) {
+ throw new GameBuilderXMLFileException(fileTitle,
+ lineNo, "<title> expected, but got <%s>", tag);
+ }
+ } else if (prevTag.equals("/title")) {
+ if (!tag.equals("price")) {
+ throw new GameBuilderXMLFileException(fileTitle,
+ lineNo, "<price> expected, but got <%s>", tag);
+ }
+ } else {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "unknown tag <%s>", tag);
+ }
+ prevTag = tag;
+ } else if (event == XmlPullParser.END_TAG) {
+ tag = "/" + parser.getName();
+ if (tag.equals("/expansion_title")) {
+ assert expansionId != null;
+ assert expansionTitle != null;
+ expansion = new Expansion(expansionId, expansionTitle,
+ preference);
+ expansionAdded = game.addExpansion(expansion);
+ } else if (tag.equals("/card")) {
+ assert expansion != null;
+ assert cardId != null;
+ assert cardTitle != null;
+ assert cardPrice != null;
+ Card card = new Card(cardId, cardTitle, cardPrice,
+ expansion);
+ game.addCard(card);
+ cardId = null;
+ cardTitle = null;
+ cardPrice = null;
+ }
+ prevTag = tag;
+ } else if (event == XmlPullParser.TEXT) {
+ String text = parser.getText();
+ if (tag.equals("expansion_title"))
+ expansionTitle = text;
+ else if (tag.equals("title"))
+ cardTitle = text;
+ else if (tag.equals("price"))
+ cardPrice = text;
+ else if (tag.equals("preference")) {
+ try {
+ preference = Integer.parseInt(text);
+ } catch (NumberFormatException e) {
+ throw new GameBuilderXMLFileException(fileTitle,
+ lineNo, "not an integer for 'preference'");
+ }
+ }
+ }
+
+ //
+ // Get the next event from the XML parser.
+ //
+ try {
+ event = parser.next();
+ } catch (Exception e) {
+ throw new GameBuilderXMLFileException(e.getCause(), fileTitle,
+ "failed to parse XML, %s", e.getMessage());
+ }
+ }
+
+ if (!tag.equals("/expansion")) {
+ throw new GameBuilderXMLFileException(fileTitle,
+ parser.getLineNumber(), "unexpected eof");
+ }
+
+ if (expansionAdded && game.getCardsSize(expansion) == 0)
+ game.removeExpansion(expansion);
+ }
+
+ //
+ // Validate attributes of an <expansion> tag.
+ //
+ private void validateExpansionStartTag(XmlPullParser parser,
+ String fileTitle, int lineNo, Expectancy exp)
+ throws GameBuilderException {
+ //
+ // Validate 'id'.
+ //
+ String expansionId = getAttribute(parser, "id");
+ if (expansionId == null) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "no 'id' attribute in <expansion>");
+ }
+ if (!expansionId.equals(exp.expansionId)) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "'id' is different from a file name");
+ }
+
+ //
+ // Validate 'game_id'.
+ //
+ String gameId = getAttribute(parser, "game_id");
+ if (gameId == null) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "no 'game_id' attribute in <expansion>");
+ }
+ if (!gameId.equals(exp.gameId)) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "'game_id' is different from a file name");
+ }
+
+ //
+ // Validate 'format_version'
+ //
+ String version = getAttribute(parser, "format_version");
+ if (version == null) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "no 'format_version' attribute in <game>");
+ }
+ boolean is_supported_version = false;
+ for (String v : SUPPORTED_XML_FORMAT_VERSIONS) {
+ if (v.equals(version)) {
+ is_supported_version = true;
+ break;
+ }
+ }
+ if (!is_supported_version) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "format_version '%s' not supported", version);
+ }
+
+ //
+ // Validate 'language'.
+ //
+ String language = getAttribute(parser, "language");
+ if (language == null) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "no 'language' attribute in <expansion>");
+ }
+ if (!language.equals(exp.language)) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "'language' is different from a file name");
+ }
+
+ //
+ // Validate 'country'.
+ //
+ String country = getAttribute(parser, "country");
+ if (country == null) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "no 'country' attribute in <expansion>");
+ }
+ if (!country.equals(exp.country)) {
+ throw new GameBuilderXMLFileException(fileTitle, lineNo,
+ "'country' is different from a file name");
+ }
+ }
+
+ //
+ // Get a file name with last 'depth' levels of an absolute path of
+ // 'file'.
+ //
+ // For example,
+ //
+ // getPartialPath(new File("/a/b/c/e/f.txt"), 2)
+ //
+ // reutrns "c/d/f.txt", the file name ("f.txt") with last 2 levels of
+ // an absolute path ("d/e") of "/a/b/c/e/f.txt".
+ //
+ private String getPartialPath(File file, int depth) {
+ String result = file.getName();
+
+ File parent = file.getParentFile();
+ for (int i = 0; i < depth && parent != null; i++) {
+ result = parent.getName() + File.separator + result;
+ parent = parent.getParentFile();
+ }
+
+ return result;
+ }
+
+ //
+ // Internal method for build() and buildAll().
+ //
+ // It searches the current start tag (START_TAG) for the specified
+ // attribute. If found, the method returns its value. Otherwise, it
+ // returns null.
+ //
+ private String getAttribute(XmlPullParser parser, String attribute) {
+ assert attribute != null;
+
+ int count = parser.getAttributeCount();
+ for (int i = 0; i < count; i++) {
+ String tag = parser.getAttributeName(i);
+ if (tag.equals(attribute))
+ return parser.getAttributeValue(i);
+ }
+ return null;
+ }
+
+ /**
+ * Converts this object to a String.
+ *
+ * @return a string "CLASS-NAME for LOCALE (reading DIR)"
+ */
+ @Override
+ public String toString() {
+ return String.format("%s (reading %s)",
+ super.toString(), directoryFile.toString());
+ }
+}