freecol/src/net/sf/freecol/common/model/StringTemplate.java

688 lines
21 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.model;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import javax.xml.stream.XMLStreamException;
import net.sf.freecol.common.i18n.Messages;
import net.sf.freecol.common.io.FreeColXMLReader;
import net.sf.freecol.common.io.FreeColXMLWriter;
import static net.sf.freecol.common.util.CollectionUtils.*;
import net.sf.freecol.common.util.Utils;
/**
* The {@code StringTemplate} represents a non-localized string
* that can be localized by looking up its value in a message bundle
* or similar Map. The StringTemplate may contain variables (keys)
* delimited by the '%' character, such as "%amount%" that will be
* replaced with a string or a StringTemplate. If the StringTemplate
* contains replacement values but no keys, then it is considered a
* "label" StringTemplate, and its value will be used to join the
* replacement values.
*
* @version 1.0
*/
public class StringTemplate extends FreeColObject {
public static final String TAG = "stringTemplate";
/** Fixed trivial return value for entryList(). */
private static final List<SimpleEntry<String,StringTemplate>> emptyList
= Collections.<SimpleEntry<String,StringTemplate>>emptyList();
/**
* The type of this StringTemplate, either NAME, a proper name
* that must not be localized (e.g. "George Washington"), or KEY,
* a string that must be localized (e.g. "model.goods.food.name"),
* or TEMPLATE, a key with replacements to apply to the localized
* value of the key, or LABEL, a separator string that will be
* used to join the replacement values.
*/
public enum TemplateType { NAME, KEY, TEMPLATE, LABEL }
/** The TemplateType of this StringTemplate. Defaults to KEY. */
private TemplateType templateType = TemplateType.KEY;
/**
* An alternative key to use if the identifier is not contained in
* the message bundle.
*/
private String defaultId = null;
/** The key,value paris to apply within the string template. */
private List<SimpleEntry<String,StringTemplate>> kv = null;
/**
* Trivial constructor to allow creation with Game.newInstance.
*/
public StringTemplate() {}
/**
* Copy an existing template, but with a new identifier.
*
* @param id The object identifier.
* @param template A {@code StringTemplate} to copy.
*/
protected StringTemplate(String id, StringTemplate template) {
setId(id);
this.templateType = template.templateType;
this.defaultId = template.defaultId;
this.kv = template.kv;
}
/**
* Creates a new {@code StringTemplate} instance.
*
* @param id The object identifier.
* @param defaultId The default identifier.
* @param templateType The {@code TemplateType} for this template.
*/
protected StringTemplate(String id, String defaultId,
TemplateType templateType) {
setId(id);
this.defaultId = defaultId;
this.templateType = templateType;
this.kv = null;
}
/**
* Create a new {@code StringTemplate} by reading a stream.
*
* @param xr The {@code FreeColXMLReader} to read.
* @exception XMLStreamException if there is a problem reading the stream.
*/
public StringTemplate(FreeColXMLReader xr) throws XMLStreamException {
readFromXML(xr);
}
// Factory methods
public static StringTemplate copy(String id, StringTemplate template) {
return new StringTemplate(id, template);
}
public static StringTemplate name(String value) {
return new StringTemplate(value, null, TemplateType.NAME);
}
public static StringTemplate key(Named named) {
return key(named.getNameKey());
}
public static StringTemplate key(String value) {
return new StringTemplate(value, null, TemplateType.KEY);
}
public static StringTemplate template(Named named) {
return template(named.getNameKey());
}
public static StringTemplate template(String value) {
return new StringTemplate(value, null, TemplateType.TEMPLATE);
}
public static StringTemplate label(String value) {
return new StringTemplate(value, null, TemplateType.LABEL);
}
/**
* Get the template type.
*
* @return The template type.
*/
public final TemplateType getTemplateType() {
return this.templateType;
}
/**
* Get the default identifier.
*
* @return The default identifier.
*/
public final String getDefaultId() {
return this.defaultId;
}
/**
* Set the default identifier.
*
* @param id The new default identifier.
*/
public final void setDefaultId(String id) {
this.defaultId = id;
}
/**
* Wrapper for subclasses to set the default identifier and return the
* setting object.
*
* @param <T> The actual return type.
* @param id The new default identifier
* @param returnClass The expected return class.
* @return The setting object.
*/
protected <T extends StringTemplate> T setDefaultId(final String id,
Class<T> returnClass) {
setDefaultId(id);
try {
return returnClass.cast(this);
} catch (ClassCastException cce) {
logger.log(Level.WARNING, "Invalid class "
+ this.getClass() + " referenced.", cce);
}
return null;
}
/**
* Has nothing been added to this template?
*
* @return True if the template is empty.
*/
public boolean isEmpty() {
return this.kv == null || this.kv.isEmpty();
}
/**
* Get the list of key, value pairs.
*
* @return The pairs.
*/
public List<SimpleEntry<String,StringTemplate>> entryList() {
return (this.kv != null) ? this.kv : emptyList;
}
/**
* Try to find the replacement for a given key.
*
* @param key The key to look for.
* @return The value found, otherwise null.
*/
public StringTemplate getReplacement(String key) {
if (this.kv == null) return null;
SimpleEntry<String,StringTemplate> val
= find(this.kv, matchKeyEquals(key, SimpleEntry::getKey));
return (val == null) ? null : val.getValue();
}
/**
* Add a key, value pair.
*
* @param key The {@code String} key.
* @param value The {@code StringTemplate} value.
*/
private void addPair(String key, StringTemplate value) {
if (key == null && value == null) {
throw new RuntimeException("Null key and pair: " + this);
}
if (this.kv == null) this.kv = new ArrayList<>();
this.kv.add(new SimpleEntry<>(key, value));
}
// @compat 0.11.x
/**
* Add a key.
*
* @param key The key to add.
*/
private void addKey(String key) {
addPair(key, null);
}
/**
* Add a replacement.
*
* @param replacement The {@code StringTemplate} replacement to add.
*/
private void addReplacement(StringTemplate replacement) {
if (this.kv == null) this.kv = new ArrayList<>();
for (SimpleEntry<String,StringTemplate> e : this.kv) {
if (e.getValue() == null) {
e.setValue(replacement);
return;
}
}
addPair(null, replacement);
}
// end @compat 0.11.x
/**
* Add an optional key and replacement. Helper function for the add*()
* routines that follow.
*
* @param <T> The actual return type.
* @param key The optional key.
* @param value The replacement {@code StringTemplate}.
* @return This object, cast back to its original class.
*/
@SuppressWarnings("unchecked")
private final <T extends StringTemplate> T complete(String key,
StringTemplate value) {
addPair(key, value);
return (T)this;
}
/**
* Add a new key and replacement value to this template.
*
* This is only possible if the template is of type TEMPLATE.
*
* @param <T> The actual return type.
* @param key The key to add.
* @param value The corresponding replacement.
* @return This.
*/
public <T extends StringTemplate> T add(String key, String value) {
if (this.templateType != TemplateType.TEMPLATE) {
throw new RuntimeException("Cannot add key-value pair"
+ " to StringTemplate." + this.templateType);
}
return complete(key, StringTemplate.key(value));
}
/**
* Add a replacement value without a key to this template.
*
* This is only possible if the template is of type LABEL.
*
* @param <T> The actual return type.
* @param value The replacement value.
* @return This.
*/
public <T extends StringTemplate> T add(String value) {
if (this.templateType != TemplateType.LABEL) {
throw new RuntimeException("Cannot add a single string"
+ " to StringTemplate." + this.templateType);
}
return complete(null, StringTemplate.key(value));
}
/**
* Add a new key and replacement proper name to this template.
*
* This is only possible if the template is of type TEMPLATE.
*
* @param <T> The actual return type.
* @param key The key to add.
* @param value The corresponding replacement.
* @return This.
*/
public <T extends StringTemplate> T addName(String key, String value) {
if (this.templateType != TemplateType.TEMPLATE) {
throw new RuntimeException("Cannot add key-name pair"
+ " to StringTemplate." + this.templateType);
}
return complete(key, StringTemplate.name(value));
}
/**
* Add a new key and replacement namable object to this template.
*
* This is only possible if the StringTemplate is of type TEMPLATE.
*
* @param <T> The actual return type.
* @param key The key to add.
* @param object The replacement {@code FreeColObject}.
* @return This.
*/
public <T extends StringTemplate> T addName(String key, FreeColObject object) {
if (this.templateType != TemplateType.TEMPLATE) {
throw new RuntimeException("Cannot add key-object pair"
+ " to StringTemplate." + this.templateType);
}
return complete(key, StringTemplate.key(Messages.nameKey(object.getId())));
}
/**
* Add a replacement proper name without a key to this template.
*
* This is only possible if the StringTemplate is of type LABEL.
*
* @param <T> The actual return type.
* @param value The replacement value.
* @return This.
*/
public <T extends StringTemplate> T addName(String value) {
if (this.templateType != TemplateType.LABEL) {
throw new RuntimeException("Cannot add a single string"
+ " to StringTemplate." + this.templateType);
}
return complete(null, StringTemplate.name(value));
}
/**
* Add a key and named object to this template.
*
* @param <T> The actual return type.
* @param key The key to add.
* @param named The {@code Named} to add.
* @return This.
*/
public <T extends StringTemplate> T addNamed(String key, Named named) {
return add(key, named.getNameKey());
}
/**
* Add named object without key to this template.
*
* @param <T> The actual return type.
* @param named The {@code Named} to add.
* @return This.
*/
public <T extends StringTemplate> T addNamed(Named named) {
return add(named.getNameKey());
}
/**
* Add a key and an integer value to replace it to this template.
*
* @param <T> The actual return type.
* @param key The key to add.
* @param amount The {@code Number} value to add.
* @return This.
*/
public <T extends StringTemplate> T addAmount(String key, Number amount) {
return addName(key, amount.toString());
}
/**
* Add a key and a replacement StringTemplate to this template.
*
* This is only possible if the StringTemplate is of type TEMPLATE.
*
* @param <T> The actual return type.
* @param key The key to add.
* @param template The {@code StringTemplate} value.
* @return This.
*/
public <T extends StringTemplate> T addStringTemplate(String key,
StringTemplate template) {
if (this.templateType != TemplateType.TEMPLATE) {
throw new RuntimeException("Cannot add key-template pair"
+ " to StringTemplate." + this.templateType);
}
return complete(key, template);
}
/**
* Add a StringTemplate to this template.
*
* This is only possible if the StringTemplate is of type LABEL.
*
* @param <T> The actual return type.
* @param template The replacement {@code StringTemplate}.
* @return This.
*/
public <T extends StringTemplate> T addStringTemplate(StringTemplate template) {
if (this.templateType != TemplateType.LABEL) {
throw new RuntimeException("Cannot add a template"
+ " to StringTemplate." + this.templateType);
}
return complete(null, template);
}
/**
* Add a tagged value.
*
* Functionally identical to add(), but used to distinguish the special
* cases at the point they are used.
*
* @param <T> The actual return type.
* @param key The tag.
* @param value The special tag value.
* @return This.
*/
public <T extends StringTemplate> T addTagged(String key, String value) {
return add(key, value);
}
// Overide FreeColObject
/**
* {@inheritDoc}
*/
@Override
public <T extends FreeColObject> boolean copyIn(T other) {
StringTemplate o = copyInCast(other, StringTemplate.class);
if (o == null || !super.copyIn(o)) return false;
this.templateType = o.getTemplateType();
this.defaultId = o.getDefaultId();
this.kv = o.kv;
return true;
}
// Serialization
private static final String DEFAULT_ID_TAG = "defaultId";
private static final String PAIR_TAG = "pair";
private static final String TEMPLATE_TYPE_TAG = "templateType";
// @compat 0.11.x
private static final String OLD_KEY_TAG = "key";
// end @compat 0.11.x
/**
* {@inheritDoc}
*/
@Override
protected void writeAttributes(FreeColXMLWriter xw) throws XMLStreamException {
super.writeAttributes(xw);
xw.writeAttribute(TEMPLATE_TYPE_TAG, templateType);
if (defaultId != null) {
xw.writeAttribute(DEFAULT_ID_TAG, defaultId);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void writeChildren(FreeColXMLWriter xw) throws XMLStreamException {
super.writeChildren(xw);
if (this.kv != null) {
for (SimpleEntry<String,StringTemplate> e : this.kv) {
xw.writeStartElement(PAIR_TAG);
String key = e.getKey(); // OK if null
if (key != null) xw.writeAttribute(VALUE_TAG, key);
e.getValue().toXML(xw); // value is always present
xw.writeEndElement();
}
}
}
/**
* {@inheritDoc}
*/
@Override
protected void readAttributes(FreeColXMLReader xr) throws XMLStreamException {
super.readAttributes(xr);
templateType = xr.getAttribute(TEMPLATE_TYPE_TAG,
TemplateType.class, TemplateType.TEMPLATE);
defaultId = xr.getAttribute(DEFAULT_ID_TAG, (String)null);
}
/**
* {@inheritDoc}
*/
@Override
protected void readChildren(FreeColXMLReader xr) throws XMLStreamException {
// Clear containers.
if (kv != null) kv.clear();
super.readChildren(xr);
}
/**
* {@inheritDoc}
*/
@Override
protected void readChild(FreeColXMLReader xr) throws XMLStreamException {
final String tag = xr.getLocalName();
if (PAIR_TAG.equals(tag)) {
// key == null is allowed
String key = xr.getAttribute(VALUE_TAG, (String)null);
StringTemplate val = null;
while (xr.moreTags()) {
final String inner = xr.getLocalName();
if (StringTemplate.TAG.equals(inner)) {
if (val == null) {
val = new StringTemplate(xr);
} else {
xr.expectTag(PAIR_TAG);
}
} else {
xr.expectTag(StringTemplate.TAG);
}
}
addPair(key, val);
// @compat 0.11.x
// old format where key and value were separated
} else if (OLD_KEY_TAG.equals(tag)) {
addKey(xr.getAttribute(VALUE_TAG, (String)null));
xr.closeTag(OLD_KEY_TAG);
} else if (StringTemplate.TAG.equals(tag)) {
addReplacement(new StringTemplate(xr));
// end @compat 0.11.x
} else {
super.readChild(xr);
}
}
/**
* {@inheritDoc}
*/
public String getXMLTagName() { return TAG; }
// Override Object
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof StringTemplate) {
StringTemplate other = (StringTemplate)o;
int i;
if (this.templateType != other.templateType
|| !Utils.equals(this.defaultId, other.defaultId))
return false;
if ((this.kv == null) != (other.kv == null)) return false;
if (this.kv != null) {
if (this.kv.size() != other.kv.size()) return false;
i = 0;
for (SimpleEntry<String,StringTemplate> e : this.kv) {
if (!e.getKey().equals(other.kv.get(i).getKey()))
return false;
if (!e.getValue().equals(other.kv.get(i).getValue()))
return false;
i++;
}
}
return super.equals(other);
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
int hash = super.hashCode();
hash = 31 * hash + this.templateType.ordinal();
hash = 31 * hash + Utils.hashCode(this.defaultId);
if (this.kv != null) {
for (SimpleEntry<String,StringTemplate> e : this.kv) {
hash = 31 * hash + Utils.hashCode(e.getKey());
hash = 31 * hash + Utils.hashCode(e.getValue());
}
}
return hash;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder(64);
sb.append(templateType).append(':').append(getId());
if (this.defaultId != null) {
sb.append('(').append(this.defaultId).append(')');
}
String start;
switch (templateType) {
case LABEL:
if (this.kv != null) {
int index = 0;
start = "[";
for (SimpleEntry<String,StringTemplate> e : this.kv) {
sb.append(start).append(index++)
.append(':').append(e.getValue());
start = " ";
}
sb.append(']');
}
break;
case TEMPLATE:
sb.append('[');
if (this.kv != null) {
start = "";
for (SimpleEntry<String,StringTemplate> e : this.kv) {
sb.append(start).append(e.getKey())
.append('=').append(e.getValue());
start = "";
}
}
sb.append(']');
break;
case KEY: case NAME: default:
break;
}
return sb.toString();
}
}