mirror of https://github.com/FreeCol/freecol.git
1178 lines
42 KiB
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;
|
|
}
|
|
}
|