freecol/src/net/sf/freecol/common/io/FreeColXMLReader.java

1178 lines
42 KiB
Java

/**
* Copyright (C) 2002-2022 The FreeCol Team
*
* This file is part of FreeCol.
*
* FreeCol 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.
*
* FreeCol 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 FreeCol. If not, see <http://www.gnu.org/licenses/>.
*/
package net.sf.freecol.common.io;
import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.stream.events.XMLEvent;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.util.StreamReaderDelegate;
import net.sf.freecol.common.model.FreeColObject;
import net.sf.freecol.common.model.FreeColGameObject;
import net.sf.freecol.common.model.FreeColSpecObjectType;
import net.sf.freecol.common.model.Game;
import net.sf.freecol.common.model.Location;
import net.sf.freecol.common.model.Specification;
import static net.sf.freecol.common.util.CollectionUtils.*;
import static net.sf.freecol.common.util.StringUtils.*;
import net.sf.freecol.server.ai.AIObject;
import net.sf.freecol.server.ai.AIMain;
/**
* A wrapper for {@code XMLStreamReader} and potentially an
* underlying stream. Adds on many useful utilities for reading
* XML and FreeCol values.
*/
public class FreeColXMLReader extends StreamReaderDelegate
implements Closeable {
private static final Logger logger = Logger.getLogger(FreeColXMLReader.class.getName());
/** Map for the XMLStreamConstants. */
private static final Map<Integer, String> tagStrings
= makeUnmodifiableMap(new Integer[] {
XMLStreamConstants.ATTRIBUTE,
XMLStreamConstants.CDATA,
XMLStreamConstants.CHARACTERS,
XMLStreamConstants.COMMENT,
XMLStreamConstants.DTD,
XMLStreamConstants.END_DOCUMENT,
XMLStreamConstants.END_ELEMENT,
XMLStreamConstants.ENTITY_DECLARATION,
XMLStreamConstants.ENTITY_REFERENCE,
XMLStreamConstants.NAMESPACE,
XMLStreamConstants.NOTATION_DECLARATION,
XMLStreamConstants.PROCESSING_INSTRUCTION,
XMLStreamConstants.SPACE,
XMLStreamConstants.START_DOCUMENT,
XMLStreamConstants.START_ELEMENT },
new String[] {
"Attribute", "CData", "Characters", "Comment", "DTD",
"EndDocument", "EndElement", "EntityDeclaration",
"EntityReference", "Namespace", "NotationDeclaration",
"ProcessingInstruction", "Space", "StartDocument",
"StartElement" });
public enum ReadScope {
SERVER, // Loading the game in the server
NORMAL, // Normal interning read
NOINTERN, // Do not intern any object that are read
}
/** Trace all reads? */
private boolean tracing = false;
/** The stream to read from. */
private InputStream inputStream = null;
/** The read scope to apply. */
private ReadScope readScope;
/**
* A cache of uninterned objects. Uninterned reads add to this list
* so that they can refer to sub-objects correctly. However there is no
* obvious place to clear this cache, so we do that in replaceScope
* as you can not expect to reference the same object across scopes.
*/
private Map<String, FreeColObject> uninterned
= new HashMap<String, FreeColObject>();
/**
* Creates a new {@code FreeColXMLReader}.
*
* @param bis The {@code BufferedInputStream} to create
* an {@code FreeColXMLReader} for.
* @exception XMLStreamException can be thrown while creating the reader.
*/
public FreeColXMLReader(BufferedInputStream bis)
throws XMLStreamException {
super();
XMLInputFactory xif = newXMLInputFactory();
XMLStreamReader xsr;
try {
xsr = xif.createXMLStreamReader(bis, "UTF-8");
setParent(xsr);
} catch (Exception ex) {
throw new XMLStreamException("Stream reader fail", ex);
}
this.inputStream = bis;
this.readScope = ReadScope.NORMAL;
this.uninterned.clear();
}
/**
* Creates a new {@code FreeColXMLReader}.
*
* @param inputStream The {@code InputStream} to create
* an {@code FreeColXMLReader} for.
* @exception XMLStreamException can be thrown while creating the reader.
*/
public FreeColXMLReader(InputStream inputStream)
throws XMLStreamException {
this(new BufferedInputStream(inputStream));
}
/**
* Creates a new {@code FreeColXMLReader}.
*
* @param file The {@code File} to create an {@code FreeColXMLReader} for.
* @exception IOException if the file is missing.
* @exception XMLStreamException can be thrown while creating the reader.
*/
public FreeColXMLReader(File file) throws IOException, XMLStreamException {
this(Files.newInputStream(file.toPath()));
}
/**
* Creates a new {@code FreeColXMLReader}.
*
* @param reader A {@code Reader} to create
* an {@code FreeColXMLReader} for.
* @exception XMLStreamException if thrown while creating the reader.
*/
public FreeColXMLReader(Reader reader) throws XMLStreamException {
super();
XMLInputFactory xif = newXMLInputFactory();
XMLStreamReader xsr = xif.createXMLStreamReader(reader);
setParent(xsr);
this.inputStream = null;
this.readScope = ReadScope.NORMAL;
this.uninterned.clear();
}
/**
* Create a new XMLInputFactory.
*
* Respond to CVE 2018-1000825.
*
* @return A new <code>XMLInputFactory</code>.
*/
private static XMLInputFactory newXMLInputFactory() {
XMLInputFactory xif = XMLInputFactory.newInstance();
// This disables DTDs entirely for that factory
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
// disable external entities
xif.setProperty("javax.xml.stream.isSupportingExternalEntities", false);
return xif;
}
/**
* Set the tracing state.
*
* @param tracing The new tracing state.
* @return This reader.
*/
public FreeColXMLReader setTracing(boolean tracing) {
this.tracing = tracing;
return this;
}
/**
* Should reads from this stream intern their objects into the
* enclosing game?
*
* @return True if this is an interning stream.
*/
public boolean shouldIntern() {
return this.readScope != ReadScope.NOINTERN;
}
/**
* Get the read scope.
*
* @return The {@code ReadScope}.
*/
public ReadScope getReadScope() {
return this.readScope;
}
/**
* Set the read scope.
*
* @param readScope The new {@code ReadScope}.
* @return This reader.
*/
public FreeColXMLReader setReadScope(ReadScope readScope) {
this.readScope = readScope;
return this;
}
/**
* Replace a the scope.
*
* @param newReadScope The {@code ReadScope} to push.
* @return The previous {@code ReadScope}.
*/
public ReadScope replaceScope(ReadScope newReadScope) {
ReadScope ret = this.readScope;
if (this.readScope != newReadScope) {
// Take the opportunity to clear the uninterned object cache
// as they can not be the same across scopes
this.uninterned.clear();
}
this.readScope = newReadScope;
return ret;
}
/**
* Look up an identifier in an enclosing game. If not interning
* prefer an non-interned result.
*
* @param game The {@code Game} to consult.
* @param id The object identifier.
* @return The {@code FreeColObject} found, or null if none.
*/
private FreeColObject lookup(Game game, String id) {
FreeColObject fco = (shouldIntern()) ? null : uninterned.get(id);
return (fco != null) ? fco
: (game == null) ? null
: game.getFreeColGameObject(id);
}
/**
* Look up an identifier in an enclosing game.
*
* This is public to allow the special fixup in Game.readChildren.
*
* @param <T> The expected object class type.
* @param game The {@code Game} to consult.
* @param id The object identifier.
* @param returnClass The class of the return value.
* @return The {@code FreeColObject} found, or null if none.
* @exception XMLStreamException if the return class does not match.
*/
public <T extends FreeColObject> T lookup(Game game, String id,
Class<T> returnClass)
throws XMLStreamException {
FreeColObject fco = lookup(game, id);
try {
return returnClass.cast(fco);
} catch (ClassCastException cce) {
throw new XMLStreamException(cce);
}
}
/**
* Closes both the {@code XMLStreamReader} and
* the underlying stream if any.
*
* Implements interface Closeable.
*/
@Override
public void close() {
try {
super.close();
} catch (XMLStreamException xse) {
logger.log(Level.WARNING, "Error closing stream.", xse);
}
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException ioe) {
logger.log(Level.WARNING, "Error closing stream.", ioe);
}
inputStream = null;
}
}
// @compat 0.11.x
/**
* Reads the identifier attribute.
*
* When all the compatibility code is obsolete, remove this
* routine and replace its uses with just:
* getAttribute(in, ID_ATTRIBUTE_TAG, (String)null)
* or equivalent.
*
* @return The identifier found, or null if none present.
*/
public String readId() {
String id = getAttribute(FreeColObject.ID_ATTRIBUTE_TAG, (String)null);
if (id == null) return null;
// @compat 0.11.x, but really 0.10.x
// Upgrade some old ids. 0.11.x never generated them, but
// games upgraded from 0.10.x could still contain them. We
// should have done this earlier, but that fell through the
// cracks...
int idx = id.indexOf(':');
if (idx > 10) {
String prefix = id.substring(0, idx);
if ("tileitemcontainer".equals(prefix)) {
id = "tileItemContainer" + id.substring(idx);
} else if ("tileimprovement".equals(prefix)) {
id = "tileImprovement" + id.substring(idx);
}
}
// @compat 0.11.x
return id;
}
// end @compat 0.11.x
/**
* {@inheritDoc}
*/
@Override
public int nextTag() throws XMLStreamException {
int tag = super.nextTag();
if (tracing) {
switch (tag) {
case XMLStreamConstants.START_ELEMENT:
System.err.println("[" + getLocalName());
break;
case XMLStreamConstants.END_ELEMENT:
System.err.println(getLocalName() + "]");
break;
default:
String val = tagStrings.get(tag);
System.err.println((val == null) ? "Weird tag: " + tag : val);
break;
}
}
return tag;
}
/**
* Is the stream at the given tag?
*
* @param tag The tag to test.
* @return True if at the given tag.
*/
public boolean atTag(String tag) {
return getLocalName().equals(tag);
}
/**
* Expect a particular tag.
*
* @param tag The expected tag name.
* @exception XMLStreamException if the expected tag is not found.
*/
public void expectTag(String tag) throws XMLStreamException {
final String endTag = getLocalName();
if (!endTag.equals(tag)) {
throw new XMLStreamException("Parse error, " + tag
+ " expected, not: " + endTag);
}
}
/**
* Check if there are more tags in the current element.
*
* @return True if the stream has not reached the end of the
* current element.
* @exception XMLStreamException if there is an error with the stream.
*/
public boolean moreTags() throws XMLStreamException {
return nextTag() != XMLStreamConstants.END_ELEMENT;
}
/**
* Close the current tag, checking that it did indeed close correctly.
*
* @param tag The expected tag name.
* @exception XMLStreamException if a closing tag is not found.
*/
public void closeTag(String tag) throws XMLStreamException {
if (moreTags()) {
throw new XMLStreamException("Parse error, END_ELEMENT expected,"
+ " not: " + getLocalName());
}
expectTag(tag);
}
/**
* Close the current tag, but accept some alternative elements first.
*
* @param tag The expected tag to close.
* @param others Alternate elements to accept.
* @exception XMLStreamException if a closing tag is not found.
*/
public void closeTag(String tag, String... others) throws XMLStreamException {
for (int next = nextTag(); next != XMLStreamConstants.END_ELEMENT;
next = nextTag()) {
String at = find(others, s -> atTag(s));
if (at == null) {
throw new XMLStreamException("Parse error, END_ELEMENT(" + tag
+ " or alternatives) expected, not: " + getLocalName());
}
closeTag(at);
}
expectTag(tag);
}
/**
* Swallow a tag, ignoring anything read until the tag closes.
*
* @param tag The tag to swallow.
* @exception XMLStreamException if a there is an error with the stream.
*/
public void swallowTag(String tag) throws XMLStreamException {
while (moreTags() || !tag.equals(getLocalName()));
}
/**
* Extract the current tag and its attributes from an input stream.
* Useful for error messages.
*
* @return A simple display of the stream state.
*/
public String currentTag() {
StringBuilder sb = new StringBuilder(getLocalName());
sb.append(", attributes:");
int n = getAttributeCount();
for (int i = 0; i < n; i++) {
sb.append(' ').append(getAttributeLocalName(i))
.append("=\"").append(getAttributeValue(i)).append('"');
}
return sb.toString();
}
/**
* Standardized way to throw a parsing exception with a bit of context.
*
* @param context A context string.
* @exception XMLStreamException is always thrown.
*/
public void unexpectedTag(String context) throws XMLStreamException {
throw new XMLStreamException("In " + context
+ ", unexpected tag " + getLocalName()
+ ", at: " + currentTag());
}
/**
* Is there an attribute present in the stream?
*
* @param attributeName An attribute name
* @return True if the attribute is present.
*/
public boolean hasAttribute(String attributeName) {
return getParent().getAttributeValue(null, attributeName) != null;
}
/**
* Gets a boolean from an attribute in a stream.
*
* @param attributeName The attribute name.
* @param defaultValue The default value.
* @return The boolean attribute value, or the default value if none found.
*/
public boolean getAttribute(String attributeName, boolean defaultValue) {
final String attrib = getParent().getAttributeValue(null,
attributeName);
return (attrib == null) ? defaultValue
: Boolean.parseBoolean(attrib);
}
/**
* Gets a float from an attribute in a stream.
*
* @param attributeName The attribute name.
* @param defaultValue The default value.
* @return The float attribute value, or the default value if none found.
*/
public float getAttribute(String attributeName, float defaultValue) {
final String attrib = getParent().getAttributeValue(null,
attributeName);
float result = defaultValue;
if (attrib != null) {
try {
result = Float.parseFloat(attrib);
} catch (NumberFormatException e) {
logger.warning(attributeName + " is not a float: " + attrib);
}
}
return result;
}
/**
* Gets an int from an attribute in a stream.
*
* @param attributeName The attribute name.
* @param defaultValue The default value.
* @return The int attribute value, or the default value if none found.
*/
public int getAttribute(String attributeName, int defaultValue) {
final String attrib = getParent().getAttributeValue(null,
attributeName);
int result = defaultValue;
if (attrib != null) {
try {
result = Integer.decode(attrib);
} catch (NumberFormatException e) {
logger.warning(attributeName + " is not an integer: " + attrib);
}
}
return result;
}
/**
* Gets a long from an attribute in a stream.
*
* @param attributeName The attribute name.
* @param defaultValue The default value.
* @return The long attribute value, or the default value if none found.
*/
public long getAttribute(String attributeName, long defaultValue) {
final String attrib = getParent().getAttributeValue(null,
attributeName);
long result = defaultValue;
if (attrib != null) {
try {
result = Long.decode(attrib);
} catch (NumberFormatException e) {
logger.warning(attributeName + " is not a long: " + attrib);
}
}
return result;
}
/**
* Gets a string from an attribute in a stream.
*
* @param attributeName The attribute name.
* @param defaultValue The default value.
* @return The string attribute value, or the default value if none found.
*/
public String getAttribute(String attributeName, String defaultValue) {
final String attrib = getParent().getAttributeValue(null,
attributeName);
return (attrib == null) ? defaultValue
: attrib;
}
/**
* Gets an enum from an attribute in a stream.
*
* @param <T> The expected enum type.
* @param attributeName The attribute name.
* @param returnClass The class of the return value.
* @param defaultValue The default value.
* @return The enum attribute value, or the default value if none found.
*/
public <T extends Enum<T>> T getAttribute(String attributeName,
Class<T> returnClass,
T defaultValue) {
final String attrib = getParent().getAttributeValue(null,
attributeName);
T result = defaultValue;
if (attrib != null) {
try {
result = Enum.valueOf(returnClass, upCase(attrib));
} catch (Exception e) {
logger.warning(attributeName + " is not a "
+ returnClass.getName() + ": " + attrib);
}
}
return result;
}
/**
* Gets a FreeCol object from an attribute in a stream.
*
* @param <T> The expected attribute type.
* @param game The {@code Game} to look in.
* @param attributeName The attribute name.
* @param returnClass The {@code FreeColObject} type to expect.
* @param defaultValue The default value.
* @return The {@code FreeColObject} found, or the default
* value if not.
* @exception XMLStreamException if the wrong class was passed.
*/
public <T extends FreeColObject> T getAttribute(Game game,
String attributeName, Class<T> returnClass,
T defaultValue) throws XMLStreamException {
final String attrib =
// @compat 0.11.x
(FreeColObject.ID_ATTRIBUTE_TAG.equals(attributeName)) ? readId() :
// end @compat 0.11.x
getAttribute(attributeName, (String)null);
if (attrib == null) return defaultValue;
return lookup(game, attrib, returnClass);
}
/**
* Get a FreeCol AI object from an attribute in a stream.
*
* @param <T> The expected attribute type.
* @param aiMain The {@code AIMain} that contains the object.
* @param attributeName The attribute name.
* @param returnClass The {@code AIObject} type to expect.
* @param defaultValue The default value.
* @return The {@code AIObject} found, or the default value if not.
*/
public <T extends AIObject> T getAttribute(AIMain aiMain,
String attributeName, Class<T> returnClass, T defaultValue) {
final String attrib =
// @compat 0.11.x
(FreeColObject.ID_ATTRIBUTE_TAG.equals(attributeName)) ? readId() :
// end @compat 0.11.x
getAttribute(attributeName, (String)null);
return (attrib == null) ? defaultValue
: aiMain.getAIObject(attrib, returnClass);
}
/**
* Find a new location from a stream attribute. This is necessary
* because {@code Location} is an interface.
*
* @param game The {@code Game} to look in.
* @param attributeName The attribute to check.
* @param make If true, try to make the location if it is not found.
* @return The {@code Location} found.
* @exception XMLStreamException if a problem was encountered
* during parsing.
*/
public Location getLocationAttribute(Game game, String attributeName,
boolean make)
throws XMLStreamException {
if (attributeName == null) return null;
final String attrib =
// @compat 0.11.x
(FreeColObject.ID_ATTRIBUTE_TAG.equals(attributeName)) ? readId() :
// end @compat 0.11.x
getAttribute(attributeName, (String)null);
if (attrib == null) return null;
FreeColObject fco = lookup(game, attrib);
if (fco == null && make) {
Class<? extends FreeColGameObject> c
= Game.getLocationClass(attrib);
if (c != null) {
fco = makeFreeColObject(game, attributeName, c,
getReadScope()==ReadScope.SERVER);
}
}
if (fco instanceof Location) return (Location)fco;
logger.warning("Not a location: " + attrib);
return null;
}
/**
* Get a map of attributes.
*
* @param attributes The list of attributes to look up.
* @return A map of attributes.
*/
public Map<String, String> getAttributeMap(String... attributes) {
Map<String, String> ret = new HashMap<>(attributes.length);
for (String a : attributes) ret.put(a, getAttribute(a, (String)null));
return ret;
}
/**
* Get a map of the array attributes.
*
* @return A map of array attributes.
*/
public Map<String, String> getArrayAttributeMap() {
Map<String, String> ret = new HashMap<>();
int n = getAttribute(FreeColObject.ARRAY_SIZE_TAG, -1);
if (n >= 0) {
ret.put(FreeColObject.ARRAY_SIZE_TAG, Integer.toString(n));
for (int i = 0; i < n; i++) {
String key = FreeColObject.arrayKey(i);
if (!hasAttribute(key)) break;
ret.put(key, getAttribute(key, (String)null));
}
}
return ret;
}
/**
* Get a map of all attributes present.
*
* @return A map of all the attributes.
*/
public Map<String, String> getAllAttributes() {
int n = getParent().getAttributeCount();
Map<String, String> ret = new HashMap<>(n);
for (int i = 0; i < n; i++) {
String key = getParent().getAttributeLocalName(i);
String value = getParent().getAttributeValue(i);
ret.put(key, value);
}
return ret;
}
/**
* Reads an XML-representation of a list of
* {@code FreeColSpecObjectType}s.
*
* @param <T> The list member type.
* @param tag The tag for the list.
* @param spec The {@code Specification} to find items in.
* @param type The type of the items to be added. The type must exist
* in the supplied specification.
* @return The list.
* @exception XMLStreamException if a problem was encountered
* during parsing.
*/
public <T extends FreeColSpecObjectType> List<T> readList(Specification spec,
String tag, Class<T> type) throws XMLStreamException {
expectTag(tag);
final int length = getAttribute(FreeColObject.ARRAY_SIZE_TAG, -1);
if (length < 0) return Collections.<T>emptyList();
List<T> list = new ArrayList<>(length);
for (int x = 0; x < length; x++) {
T value = getType(spec, FreeColObject.arrayKey(x), type, (T)null);
if (value == null) logger.warning("Null list value(" + x + ")");
list.add(value);
}
closeTag(tag);
return list;
}
/**
* Find a {@code FreeColGameObject} of a given class
* from a stream attribute.
*
* Use this routine when the object is optionally already be
* present in the game.
*
* @param <T> The actual return type.
* @param game The {@code Game} to look in.
* @param attributeName The attribute name.
* @param returnClass The class to expect.
* @param defaultValue A default value to return if not found.
* @param required If true a null result should throw an exception.
* @return The {@code FreeColGameObject} found, or the default
* value if not found.
* @exception XMLStreamException if the attribute is missing.
*/
public <T extends FreeColGameObject> T findFreeColGameObject(Game game,
String attributeName, Class<T> returnClass, T defaultValue,
boolean required) throws XMLStreamException {
T ret = getAttribute(game, attributeName, returnClass, (T)null);
if (ret == (T)null) {
if (required) {
throw new XMLStreamException("Missing " + attributeName
+ " for " + returnClass.getName() + ": " + currentTag());
} else {
ret = defaultValue;
}
}
return ret;
}
/**
* Either get an existing {@code FreeColObject} from a stream
* attribute or create it if it does not exist.
*
* Use this routine when the object may not necessarily already be
* present in the game, but is expected to be defined eventually.
*
* @param <T> The actual return type.
* @param game The {@code Game} to look in.
* @param attributeName The required attribute name.
* @param returnClass The class of object.
* @param required If true a null result should throw an exception.
* @return The {@code FreeColObject} found or made, or null
* if the attribute was not present.
* @exception XMLStreamException if a problem was encountered
* during parsing.
*/
public <T extends FreeColObject> T makeFreeColObject(Game game,
String attributeName, Class<T> returnClass, boolean required)
throws XMLStreamException {
final String id =
// @compat 0.11.x
(FreeColObject.ID_ATTRIBUTE_TAG.equals(attributeName)) ? readId() :
// end @compat 0.11.x
getAttribute(attributeName, (String)null);
T ret = null;
if (id == null) {
if (required) {
throw new XMLStreamException("Missing " + attributeName
+ " for " + returnClass.getName() + ": " + currentTag());
}
} else if ((ret = lookup(game, id, returnClass)) == null) {
ret = Game.newInstance(game, returnClass,
getReadScope() == ReadScope.SERVER);
if (ret == null) {
String err = "Failed to create " + returnClass.getName()
+ " with id: " + id;
if (required) throw new XMLStreamException(err);
logger.warning(err);
} else if (ret instanceof FreeColGameObject) {
// Do not set the id earlier or interning will happen
// by default in the constructor called from newInstance
ret.setId(id);
if (shouldIntern()) {
((FreeColGameObject)ret).internId(id);
} else {
uninterned.put(id, ret);
}
}
}
return ret;
}
/**
* Do a normal interning read of a {@code FreeColObject}.
*
* @param <T> The actual return type.
* @param game The {@code Game} to look in.
* @param returnClass The class to expect.
* @return The {@code FreeColObject} found, or null there
* was no ID_ATTRIBUTE_TAG present.
* @exception XMLStreamException if there is problem reading the stream.
*/
private <T extends FreeColObject> T internedRead(Game game,
Class<T> returnClass) throws XMLStreamException {
T ret = makeFreeColObject(game, FreeColObject.ID_ATTRIBUTE_TAG,
returnClass, false);
if (ret != null) ret.readFromXML(this);
return ret;
}
/**
* Do a special non-interning read of a {@code FreeColObject}.
*
* @param <T> The actual return type.
* @param game The {@code Game} to look in.
* @param returnClass The class to expect.
* @return The {@code FreeColObject} found, or null there
* was no ID_ATTRIBUTE_TAG present.
* @exception XMLStreamException if there is problem reading the stream.
*/
private <T extends FreeColObject> T uninternedRead(Game game,
Class<T> returnClass) throws XMLStreamException {
String id = readId();
if (id == null) {
throw new XMLStreamException("Null object identifier for: " + returnClass.getName());
}
T ret;
FreeColObject fco = uninterned.get(id);
if (fco == null) {
ret = Game.newInstance(game, returnClass,
getReadScope() == ReadScope.SERVER);
if (ret == null) {
throw new XMLStreamException("Failed to create "
+ returnClass.getName() + " with id: " + id);
}
} else {
try {
ret = returnClass.cast(fco);
} catch (ClassCastException cce) {
throw new XMLStreamException(cce);
}
}
uninterned.put(id, ret); // Register id before reading
ret.readFromXML(this);
return ret;
}
/**
* Reads a {@code FreeColObject} from a stream.
* Expects the object to be identified by the standard ID_ATTRIBUTE_TAG.
*
* Use this routine when the object may or may not have been
* referenced and created-by-id in this game, but this is the
* point where it is authoritatively defined.
*
* @param <T> The actual return type.
* @param game The {@code Game} to look in.
* @param returnClass The class to expect.
* @return The {@code FreeColObject} found, or null there
* was no ID_ATTRIBUTE_TAG present.
* @exception XMLStreamException if there is problem reading the stream.
*/
public <T extends FreeColObject> T readFreeColObject(Game game,
Class<T> returnClass) throws XMLStreamException {
return (shouldIntern())
? internedRead(game, returnClass)
: uninternedRead(game, returnClass);
}
/**
* Read a {@code FreeColObject} from a stream.
*
* @param <T> The actual return type.
* @param game The {@code Game} to look in.
* @return The {@code FreeColObject} found.
* @exception XMLStreamException if there is problem reading the stream.
*/
public <T extends FreeColObject> T readFreeColObject(Game game)
throws XMLStreamException {
final String tag = getLocalName();
Class<T> returnClass = FreeColObject.getFreeColObjectClassByName(tag);
if (returnClass == null) {
throw new XMLStreamException("No class: " + tag);
}
return readFreeColObject(game, returnClass);
}
/**
* Find a FreeCol AI object from an attribute in a stream.
*
* @param <T> The actual return type.
* @param aiMain The {@code AIMain} that contains the object.
* @param attributeName The attribute name.
* @param returnClass The {@code AIObject} type to expect.
* @param defaultValue The default value.
* @param required If true a null result should throw an exception.
* @exception XMLStreamException if there is problem reading the stream.
* @return The {@code AIObject} found, or the default value if not.
*/
public <T extends AIObject> T findAIObject(AIMain aiMain,
String attributeName, Class<T> returnClass, T defaultValue,
boolean required) throws XMLStreamException {
T ret = getAttribute(aiMain, attributeName, returnClass, (T)null);
if (ret == (T)null) {
if (required) {
throw new XMLStreamException("Missing " + attributeName
+ " for " + returnClass.getName() + ": " + currentTag());
} else {
ret = defaultValue;
}
}
return ret;
}
/**
* Either get an existing {@code AIObject} from a stream
* attribute or create it if it does not exist.
*
* Use this routine when the object may not necessarily already be
* present in the game, but is expected to be defined eventually.
*
* @param <T> The actual return type.
* @param aiMain The {@code AIMain} that contains the object.
* @param attributeName The attribute name.
* @param returnClass The {@code AIObject} type to expect.
* @param defaultValue The default value.
* @param required If true, throw exceptions on missing data.
* @exception XMLStreamException if there is problem reading the stream.
* @return The {@code AIObject} found, or the default value if not.
*/
public <T extends AIObject> T makeAIObject(AIMain aiMain,
String attributeName, Class<T> returnClass, T defaultValue,
boolean required) throws XMLStreamException {
final String id =
// @compat 0.11.x
(FreeColObject.ID_ATTRIBUTE_TAG.equals(attributeName)) ? readId() :
// end @compat 0.11.x
getAttribute(attributeName, (String)null);
T ret = null;
if (id == null) {
if (required) {
throw new XMLStreamException("Missing " + attributeName
+ " for " + returnClass.getName() + ": " + currentTag());
}
} else {
ret = aiMain.getAIObject(id, returnClass);
if (ret == null) {
try {
Constructor<T> c = returnClass.getConstructor(AIMain.class,
String.class);
ret = returnClass.cast(c.newInstance(aiMain, id));
if (required && ret == null) {
throw new XMLStreamException("Constructed null "
+ returnClass.getName() + " for " + id
+ ": " + currentTag());
}
} catch (NoSuchMethodException | SecurityException
| InstantiationException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException
| XMLStreamException e) {
if (required) {
throw new XMLStreamException(e);
} else {
logger.log(Level.WARNING, "Failed to create AIObject: "
+ id, e);
}
}
}
}
return ret;
}
/**
* Should the game object type being read clear its containers before
* reading the child elements?
*
* @return True if the containers should be cleared.
*/
public boolean shouldClearContainers() {
return !getAttribute(FreeColSpecObjectType.PRESERVE_TAG, false);
}
/**
* Should the attributes of the game object type be read?
*
* @return True if the attributes should be read.
*/
public boolean shouldReadAttributes() {
return !getAttribute(FreeColSpecObjectType.PRESERVE_ATTRIBUTES_TAG, false);
}
/**
* Get a FreeColSpecObjectType by identifier from a stream from a
* specification.
*
* @param <T> The actual return type.
* @param spec The {@code Specification} to look in.
* @param attributeName the name of the attribute identifying the
* {@code FreeColSpecObjectType}.
* @param returnClass The expected class of the return value.
* @param defaultValue A default value to return if the attributeName
* attribute is not present.
* @return The {@code FreeColSpecObjectType} found, or the
* {@code defaultValue}.
*/
public <T extends FreeColSpecObjectType> T getType(Specification spec,
String attributeName, Class<T> returnClass, T defaultValue) {
final String attrib =
// @compat 0.11.x
(FreeColObject.ID_ATTRIBUTE_TAG.equals(attributeName)) ? readId() :
// end @compat 0.11.x
getAttribute(attributeName, (String)null);
return (attrib == null) ? defaultValue
: spec.findType(attrib, returnClass);
}
/**
* Get an initialized FreeColSpecObjectType by identifier from a stream from a
* specification.
*
* @param <T> The actual return type.
* @param spec The {@code Specification} to look in.
* @param attributeName the name of the attribute identifying the
* {@code FreeColSpecObjectType}.
* @param returnClass The expected class of the return value.
* @param defaultValue A default value to return if the attributeName
* attribute is not present.
* @return The {@code FreeColSpecObjectType} found, or the
* {@code defaultValue}.
*/
public <T extends FreeColSpecObjectType> T getAlreadyInitializedType(Specification spec,
String attributeName, Class<T> returnClass, T defaultValue) {
final String id = getAttribute(attributeName, (String)null);
if (id == null) {
return defaultValue;
}
final T o = spec.getAlreadyInitializedType(id, returnClass);
if (o == null) {
throw new IllegalArgumentException(String.format(
"The object \"%s\" of type \"%s\" was referenced from a \"%s\" attribute before being initialized.",
id, returnClass.getSimpleName(), attributeName
));
}
return o;
}
/**
* Copy a FreeColObject by serializing it and reading back the result
* with a non-interning stream.
*
* @param <T> The actual return type.
* @param game The {@code Game} to look in.
* @param returnClass The class to expect.
* @return The copied {@code FreeColObject} found, or null there
* was no ID_ATTRIBUTE_TAG present.
* @exception XMLStreamException if there is problem reading the stream.
*/
public <T extends FreeColObject> T copy(Game game, Class<T> returnClass)
throws XMLStreamException {
setReadScope(ReadScope.NOINTERN);
nextTag();
return uninternedRead(game, returnClass);
}
/**
* Fill in the identifier values supplied in a map.
*
* Special case for early reads of the client options.
*
* @param map A map containing identifiers to find as keys.
* @param attr The attribute to get for each identifier found.
* @return The number of identifiers found or negative on error.
* @exception XMLStreamException if a problem was encountered
* during parsing.
*/
public int readAttributeValues(Map<String, String> map,
String attr) throws XMLStreamException {
int ret = 0;
while (hasNext()) {
String id;
try {
nextTag();
} catch (XMLStreamException xse) {
break;
}
if (getEventType() == XMLEvent.START_ELEMENT
&& (id = readId()) != null
&& map.containsKey(id)) {
map.put(id, getAttribute(attr, (String)null));
ret++;
}
}
return ret;
}
}