/* $Id$
*
* Copyright (c) 2008 by Brent Easton and Joel Uckelman
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License (LGPL) as published by the Free Software Foundation.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, copies are available
* at http://www.opensource.org.
*/
package VASSAL.build.module.metadata;
import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
import VASSAL.Info;
import VASSAL.build.Configurable;
import VASSAL.build.GameModule;
import VASSAL.i18n.Translation;
import VASSAL.tools.ArchiveWriter;
import VASSAL.tools.ErrorDialog;
import VASSAL.tools.io.FastByteArrayOutputStream;
import VASSAL.tools.io.FileArchive;
import VASSAL.tools.io.IOUtils;
/**
*
* Base class representing the metadata for a Saved Game, Module or Extension.
*
* @author Brent Easton
* @since 3.1.0
*/
public abstract class AbstractMetaData {
private static final Logger logger =
LoggerFactory.getLogger(AbstractMetaData.class);
protected static final String TRUE = "true";
protected static final String FALSE = "false";
protected static final String NAME_ATTR = "name";
protected static final String VERSION_ATTR = "version";
protected static final String VASSAL_VERSION_ATTR = "vassalVersion";
protected static final String DESCRIPTION_ATTR = "description";
protected static final String EXTENSION_ATTR = "extension";
protected static final String MODULE_NAME_ATTR = "moduleName";
protected static final String MODULE_VERSION_ATTR = "moduleVersion";
protected static final String LANG_ATTR = "lang";
protected static final String ROOT_ELEMENT = "data";
protected static final String VERSION_ELEMENT = "version";
protected static final String VASSAL_VERSION_ELEMENT = "VassalVersion";
protected static final String MODULE_NAME_ELEMENT = "moduleName";
protected static final String MODULE_VERSION_ELEMENT = "moduleVersion";
protected static final String DESCRIPTION_ELEMENT = "description";
protected static final String NAME_ELEMENT = "name";
protected static final String DATE_SAVED_ELEMENT = "dateSaved";
protected static final String BUILDFILE_MODULE_ELEMENT1 = "VASSAL.launch.BasicModule";
protected static final String BUILDFILE_MODULE_ELEMENT2 = "VASSAL.build.GameModule";
protected static final String BUILDFILE_EXTENSION_ELEMENT = "VASSAL.build.module.ModuleExtension";
protected String version;
protected String vassalVersion;
protected Attribute descriptionAttr;
public AbstractMetaData() {
setVassalVersion(Info.getVersion());
}
public String getVersion() {
return version == null ? "" : version;
}
public void setVersion(String s) {
version = s;
}
public String getVassalVersion() {
return vassalVersion == null ? "" : vassalVersion;
}
public void setVassalVersion(String s) {
vassalVersion = s;
}
public void setDescription(Attribute desc) {
descriptionAttr = desc;
}
public void setDescription(String desc) {
descriptionAttr = new Attribute(GameModule.DESCRIPTION, desc);
}
public String getDescription() {
return descriptionAttr == null ? "" : descriptionAttr.getValue();
}
public String getLocalizedDescription() {
return descriptionAttr == null ? "" : descriptionAttr.getLocalizedValue();
}
public void save(FileArchive archive) throws IOException {
OutputStream out = null;
try {
out = archive.getOutputStream(getZipEntryName());
save(out);
out.close();
}
finally {
IOUtils.closeQuietly(out);
}
}
protected void save(OutputStream out) throws IOException {
Document doc = null;
Element e = null;
try {
doc = DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.newDocument();
final Element root = doc.createElement(ROOT_ELEMENT);
root.setAttribute(VERSION_ATTR, getMetaDataVersion());
doc.appendChild(root);
if (getVersion() != null) {
e = doc.createElement(VERSION_ELEMENT);
e.appendChild(doc.createTextNode(getVersion()));
root.appendChild(e);
}
if (getVassalVersion() != null) {
e = doc.createElement(VASSAL_VERSION_ELEMENT);
e.appendChild(doc.createTextNode(getVassalVersion()));
root.appendChild(e);
}
e = doc.createElement(DATE_SAVED_ELEMENT);
e.appendChild(doc.createTextNode(
String.valueOf(System.currentTimeMillis())));
root.appendChild(e);
if (descriptionAttr != null) {
descriptionAttr.generateXML(doc, root, DESCRIPTION_ELEMENT);
}
addElements(doc, root);
}
catch (ParserConfigurationException ex) {
ErrorDialog.bug(ex);
// FIXME: switch to IOException(Throwable) ctor in Java 1.6
throw (IOException) new IOException().initCause(ex);
}
try {
final Transformer xformer =
TransformerFactory.newInstance().newTransformer();
xformer.setOutputProperty(OutputKeys.INDENT, "yes");
xformer.setOutputProperty(
"{http://xml.apache.org/xslt}indent-amount", "2");
xformer.transform(new DOMSource(doc), new StreamResult(out));
}
catch (TransformerConfigurationException ex) {
ErrorDialog.bug(ex);
// FIXME: switch to IOException(Throwable) ctor in Java 1.6
throw (IOException) new IOException().initCause(ex);
}
catch (TransformerFactoryConfigurationError ex) {
ErrorDialog.bug(ex);
// FIXME: switch to IOException(Throwable) ctor in Java 1.6
throw (IOException) new IOException().initCause(ex);
}
catch (TransformerException ex) {
// FIXME: switch to IOException(Throwable) ctor in Java 1.6
throw (IOException) new IOException().initCause(ex);
}
}
/**
* Write common metadata to the specified Archive. Call addElements to
* add elements specific to particular concrete subclasses.
*
* @param archive
* Extension Archive
* @throws IOException
* If anything goes wrong
*/
public void save(ArchiveWriter archive) throws IOException {
final FastByteArrayOutputStream out = new FastByteArrayOutputStream();
save(out);
archive.addFile(getZipEntryName(), out.toInputStream());
}
/**
* Copy the Module metatdata from the current module into the specified
* archive.
*
* @param archive Archive to copy into
* @throws IOException
*/
public void copyModuleMetadata(ArchiveWriter archive) throws IOException {
copyModuleMetadata(archive.getArchive());
}
public void copyModuleMetadata(FileArchive archive) throws IOException {
BufferedInputStream in = null;
try {
in = new BufferedInputStream(
GameModule.getGameModule()
.getDataArchive()
.getInputStream(ModuleMetaData.ZIP_ENTRY_NAME));
archive.add(ModuleMetaData.ZIP_ENTRY_NAME, in);
in.close();
}
catch (FileNotFoundException e) {
// No Metatdata in source module, create a fresh copy
new ModuleMetaData(GameModule.getGameModule()).save(archive);
}
finally {
IOUtils.closeQuietly(in);
}
}
/**
* Return the Entry name for the metatdata file
*
* @return Zip Entry name
*/
public abstract String getZipEntryName();
/**
* Return the version of the metadata structure
*
* @return version
*/
public abstract String getMetaDataVersion();
/**
* Add elements specific to a MetaData subclass
*
* @param doc Document
* @param root Root element
*/
protected abstract void addElements(Document doc, Element root);
/*************************************************************************
* Utility Exception class, used to cancel SAX parsing
*
*/
static class SAXEndException extends SAXException {
private static final long serialVersionUID = 1L;
}
/*************************************************************************
* Utility class representing a Configurable attribute and its translations
*
*/
static class Attribute {
protected String attributeName;
protected String value;
protected HashMap<String, String> translations =
new HashMap<String, String>();
/**
* Build Attribute class based on atrribute value and translations
* available in the current module
*
* @param c Target configurable
* @param name Attribute name
*/
public Attribute(Configurable target, String name) {
attributeName = name;
value = target.getAttributeValueString(attributeName);
String key = target.getI18nData().getFullPrefix();
if (key.length()> 0) key += ".";
key += attributeName;
for (Translation t : GameModule.getGameModule().getAllDescendantComponentsOf(Translation.class)) {
addTranslation(t.getLanguageCode(), t.translate(key));
}
}
public Attribute (String attributeName, String value) {
this.attributeName = attributeName;
this.value = value;
}
public void addTranslation(String language, String value) {
if (value != null) {
translations.put(language, value);
}
}
/**
* Return the untranslated value of this attribute
*
* @return value
*/
public String getValue() {
return value;
}
/**
* Return the value of this attribute translated into the local
* language
*
* @return translated value
*/
public String getLocalizedValue() {
String lang = Locale.getDefault().getLanguage();
String tx = translations.get(lang);
return tx == null ? getValue() : tx;
}
/**
* Output metadata XML for this attribute, including translations
*
* @param doc Root document
* @param root Parent element
* @param prefix Attribute prefix
*/
public void generateXML(Document doc, Element root, String prefix) {
if (value == null) {
return;
}
Element e = null;
e = doc.createElement(prefix);
e.appendChild(doc.createTextNode(value));
root.appendChild(e);
for (Map.Entry<String,String> en : translations.entrySet()) {
e = doc.createElement(prefix);
e.setAttribute(LANG_ATTR, en.getValue());
e.appendChild(doc.createTextNode(en.getKey()));
root.appendChild(e);
}
}
}
/**
* This is the shared parser for all subclasses of AbstractMetaData.
* We use a shared parser because the call to
* {@link XMLReaderFactory.createXMLReader()} is extremely expensive.
* All uses of this parser <i>must</i> be wrapped in a block synchronized
* on the parser itself.
*/
protected static XMLReader parser;
// FIXME: Synchronizing on the parser will cause very bad performance if
// multiple threads are trying to read metadata simultaneously. We should
// build a mechanism by which we keep a pool of parsers, and allocate a
// new one only when there is not an unused one available in the pool.
static {
try {
parser = XMLReaderFactory.createXMLReader();
}
catch (SAXException e) {
// This should never happen.
ErrorDialog.bug(e);
}
}
/*************************************************************************
* Base XML Handler for all metadata classes
*
*/
class XMLHandler extends DefaultHandler {
final StringBuilder accumulator = new StringBuilder();
protected String language = "";
@Override
public void startElement(String uri, String localName,
String qName, Attributes attrs) {
// clear the content accumulator
accumulator.setLength(0);
// Track language for localizable attributes
language = getAttr(attrs, LANG_ATTR);
}
protected String getAttr(Attributes attrs, String qName) {
final String value = attrs.getValue(qName);
return value == null ? "" : value;
}
public void endElement(String uri, String localName, String qName) {
// handle all of the elements which have CDATA here
String value = accumulator.toString().trim();
if (VERSION_ELEMENT.equals(qName)) {
setVersion(value);
}
else if (VASSAL_VERSION_ELEMENT.equals(qName)) {
setVassalVersion(value);
}
else if (DESCRIPTION_ELEMENT.equals(qName)) {
if (descriptionAttr == null) {
setDescription(new Attribute(DESCRIPTION_ELEMENT, value));
}
else {
descriptionAttr.addTranslation(language, value);
}
}
}
@Override
public void characters(char[] ch, int start, int length) {
accumulator.append(ch, start, length);
}
@Override
public void warning(SAXParseException e) throws SAXException {
logger.warn("", e);
}
@Override
public void error(SAXParseException e) throws SAXException {
logger.error("", e);
}
@Override
public void fatalError(SAXParseException e) throws SAXException {
throw e;
}
}
/*************************************************************************
* XML Handler for parsing a buildFile. Used to read minimal data from
* modules saved prior to 3.1.0.
*/
class BuildFileXMLHandler extends DefaultHandler {
final StringBuilder accumulator = new StringBuilder();
@Override
public void startElement(String uri, String localName,
String qName, Attributes attrs)
throws SAXEndException {
// clear the content accumulator
accumulator.setLength(0);
}
protected String getAttr(Attributes attrs, String qName) {
final String value = attrs.getValue(qName);
return value == null ? "" : value;
}
@Override
public void endElement(String uri, String localName, String qName) {
// handle all of the elements which have CDATA here
}
@Override
public void characters(char[] ch, int start, int length) {
accumulator.append(ch, start, length);
}
@Override
public void warning(SAXParseException e) throws SAXException {
logger.warn("", e);
}
@Override
public void error(SAXParseException e) throws SAXException {
logger.error("", e);
}
@Override
public void fatalError(SAXParseException e) throws SAXException {
throw e;
}
}
}