/**
* eAdventure (formerly <e-Adventure> and <e-Game>) is a research project of the
* <e-UCM> research group.
*
* Copyright 2005-2010 <e-UCM> research group.
*
* You can access a list of all the contributors to eAdventure at:
* http://e-adventure.e-ucm.es/contributors
*
* <e-UCM> is a research group of the Department of Software Engineering
* and Artificial Intelligence at the Complutense University of Madrid
* (School of Computer Science).
*
* C Profesor Jose Garcia Santesmases sn,
* 28040 Madrid (Madrid), Spain.
*
* For more info please visit: <http://e-adventure.e-ucm.es> or
* <http://www.e-ucm.es>
*
* ****************************************************************************
*
* This file is part of eAdventure, version 2.0
*
* eAdventure is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* eAdventure 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with eAdventure. If not, see <http://www.gnu.org/licenses/>.
*/
package es.eucm.ead.editor.model;
import com.google.inject.Inject;
import es.eucm.ead.editor.EditorStringHandler;
import es.eucm.ead.editor.model.nodes.*;
import es.eucm.ead.editor.model.nodes.asset.AssetFactory;
import es.eucm.ead.editor.model.nodes.asset.AssetsNode;
import es.eucm.ead.editor.model.visitor.ModelVisitor;
import es.eucm.ead.editor.model.visitor.ModelVisitorDriver;
import es.eucm.ead.editor.util.DataPrettifier;
import es.eucm.ead.importer.AdventureConverter;
import es.eucm.ead.importer.annotation.ImportAnnotator;
import es.eucm.ead.model.elements.AdventureGame;
import es.eucm.ead.reader.AdventureReader;
import es.eucm.ead.reader.strings.StringsReader;
import es.eucm.ead.tools.PropertiesReader;
import es.eucm.ead.tools.StringHandler;
import es.eucm.ead.tools.TextFileReader;
import es.eucm.ead.tools.java.JavaTextFileReader;
import es.eucm.ead.tools.java.JavaTextFileWriter;
import es.eucm.ead.tools.java.utils.FileUtils;
import es.eucm.ead.tools.reflection.ReflectionProvider;
import es.eucm.ead.tools.xml.XMLParser;
import es.eucm.ead.writer.AdventureWriter;
import es.eucm.ead.writer.StringWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.*;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.zip.ZipFile;
/**
* Loads an EditorModel.
*
* EditorModels contain each 'Identified' element in the XML wrapped up in a
* DependencyNode (which hosts lookup information and dependencies).
* Non-identified elements (maps and lists) are provided transient,
* lookup-by-value DependencyNodes, as are
*
* Imported editorModels are passed through a series of factories to build
* EditorNodes. Loaded models have these EditorNodes restored from their
* editor.xml file.
*
* @author mfreire
*/
public class EditorModelLoader {
private static final Logger logger = LoggerFactory
.getLogger(EditorModelLoader.class);
/**
* Importer for old models
*/
private final AdventureConverter converter;
/**
* True only during a loading operation
*/
private boolean isLoading = false;
/**
* Reader for DOM models
*/
private final AdventureReader reader;
/**
* Reader for zipped DOM models
*/
private final AdventureReader zipReader;
/**
* Internal zip-file reader, used to provide folder-like access to zip files
*/
private final ZipTextFileReader zipTextFileReader;
/**
* Writer for DOM models
*/
private final AdventureWriter writer;
/**
* Parser for XML documents
*/
private final XMLParser parser;
/**
* Current project directory; used to save & load
*/
private File saveDir;
/**
* An import annotator that can reconstitute a bit of an existing import
*/
private final EditorAnnotator importAnnotator;
/**
* A list of editor node factories for imports
*/
private final ArrayList<EditorNodeFactory> importNodeFactories = new ArrayList<EditorNodeFactory>();
/**
* Name of file with editor-node descriptions
*/
private static final String editorModelFile = "editor.xml";
/**
* Name of file with strings
*/
private static final String stringsFile = "strings.xml";
/**
* Model to load or save; must be set before any operation is performed
*/
private EditorModelImpl model;
@Inject
public EditorModelLoader(XMLParser parser, ImportAnnotator annotator,
ReflectionProvider reflectionProvider) {
this.parser = parser;
this.reader = new AdventureReader(reflectionProvider, parser,
new JavaTextFileReader());
this.zipTextFileReader = new ZipTextFileReader();
this.zipReader = new AdventureReader(reflectionProvider, parser,
zipTextFileReader);
this.writer = new AdventureWriter(reflectionProvider);
this.importAnnotator = (EditorAnnotator) annotator;
this.saveDir = null;
this.converter = new AdventureConverter();
importNodeFactories.add(new ActorFactory());
importNodeFactories.add(new AssetFactory());
importNodeFactories.add(new SceneFactory());
}
public void setModel(EditorModelImpl model) {
this.model = model;
}
/**
* Useful for mocking save dirs.
* @param saveDir to use instead of default or previous.
*/
public void setSaveDir(File saveDir) {
this.saveDir = saveDir;
}
/**
* Exports the editor model into a zip file.
*
* @param target
* ; if null, previous target is assumed
* @throws IOException
* //FIXME: Folder
*/
public void exportGame(File target) throws IOException {
writer.write((AdventureGame) model.getEngineModel(), saveDir
.getAbsolutePath(), new JavaTextFileWriter());
// target.getAbsolutePath(), ".eap",
// "Editor project, exported", true);
// always adds the mappings, to allow editing it once again
// writeEditorNodes(target);
}
/**
* Writes the editor mappings to an editor.xml file.
*
* @param dest
* @return number of mappings written
*/
private int writeEditorNodes(File target) throws IOException {
int mappings = 0;
StringBuilder sb = new StringBuilder("<editorNodes>\n");
for (DependencyNode n : model.getNodesById().values()) {
if (n instanceof EditorNode) {
logger.debug("Writing editorNode of type {} with id {}",
new Object[] { n.getClass(), n.getId() });
((EditorNode) n).write(sb);
mappings++;
}
}
ByteArrayInputStream bis = new ByteArrayInputStream(sb.append(
"</editorNodes>\n").toString().getBytes("UTF-8"));
OutputStream fos;
if (target.isFile()) {
FileUtils.appendEntryToZip(target, editorModelFile, bis);
} else {
FileUtils.writeToFile(bis, new File(target, editorModelFile));
}
return mappings;
}
/**
* Reads the editor mappings from an editor.xml file.
*
* @param source
* @return number of mappings read
*/
private int readEditorNodes(File source) throws IOException {
InputStream input;
if (source.isFile()) {
ZipFile zip = new ZipFile(source);
input = FileUtils.readEntryFromZip(zip, editorModelFile);
} else {
input = new BufferedInputStream(new FileInputStream(new File(
source, editorModelFile)));
}
int read = 0;
try {
Document doc = DocumentBuilderFactory.newInstance()
.newDocumentBuilder().parse(input);
ClassLoader cl = this.getClass().getClassLoader();
NodeList nodes = doc.getElementsByTagName("node");
logger.debug("Parsed {} fine; {} mappings read OK", new Object[] {
source, nodes.getLength() });
// build
for (int i = 0; i < nodes.getLength(); i++) {
Element e = (Element) nodes.item(i);
String className = e.getAttribute("class");
int id = Integer.valueOf(e.getAttribute("id"));
logger.debug("\trestoring {} {}",
new Object[] { className, id });
EditorNode editorNode = (EditorNode) cl.loadClass(className)
.getConstructor(Integer.TYPE).newInstance(id);
model.getNodesById().put(id, editorNode);
}
// initialize
DependencyNode[] changed = new DependencyNode[nodes.getLength()];
for (int i = 0; i < nodes.getLength(); i++) {
Element e = (Element) nodes.item(i);
int id = Integer.valueOf(e.getAttribute("id"));
EditorNode editorNode = (EditorNode) model.getNodesById().get(
id);
String childrenIds = e.getAttribute("contents");
logger.debug("\tinitializing {}, {}", new Object[] { id,
childrenIds });
for (String idString : childrenIds.split("[,]")) {
if (idString.isEmpty()) {
// this can happen if there are no values...
continue;
}
int cid = Integer.valueOf(idString);
logger.debug("\tadding child {}", cid);
if (model.getNodesById().get(cid) == null) {
logger
.error(
"Cannot add child {} of editorNode {}: null child (id {} not registered)",
new Object[] { idString, id, idString });
} else {
editorNode.addChild(model.getNodesById().get(cid));
logger.debug("\tadding child {} [{}]", new Object[] {
cid,
model.getNodesById().get(cid)
.getTextualDescription(model) });
}
}
editorNode.restoreInner(e, model);
model.registerEditorNodeWithGraph(editorNode);
changed[read++] = editorNode;
}
// update index
model.fireModelEvent(new DefaultModelEvent("editor-load", this,
null, null, changed));
} catch (Exception e) {
logger.error("Error reading mappings from file {}", source, e);
}
return read;
}
private class EditorModelVisitor implements ModelVisitor {
/**
* Visits a node
*
* @see ModelVisitor#visitObject
*/
@Override
public boolean visitObject(Object target, Object source,
String sourceName) {
logger.debug("Visiting object: '{}'--['{}']-->'{}'", new Object[] {
source, sourceName, target });
// source is only null for root node
if (source == null) {
// should keep on drilling, but otherwise nothing to do here
model.addNode(null, null, target, isLoading);
return true;
}
DependencyNode sourceNode = model.getNodeFor(source);
DependencyNode e = model.addNode(sourceNode, sourceName, target,
isLoading);
if (e != null) {
model.getNodeIndex().addProperty(e,
ModelIndex.editorIdFieldName, "" + e.getId(), false);
return true;
} else {
// already exists in graph; in this case, do not drill deeper
return false;
}
}
/**
* Visits a node property. Mostly used for indexing
*
* @see ModelVisitor#visitProperty
*/
@Override
public void visitProperty(Object target, String propertyName,
String textValue) {
logger.debug("Visiting property: '{}' :: '{}' = '{}'",
new Object[] { target, propertyName, textValue });
DependencyNode targetNode = model.getNodeFor(target);
model.getNodeIndex().addProperty(targetNode, propertyName,
textValue, true);
}
}
/**
* Builds EditorNodes from EngineNodes. Requires a 'hot' (recently updated)
* EditorAnnotator. Discards annotator information after use.
*/
private void createEditorNodes() {
// engine ids may have changed during load
importAnnotator.rebuild();
for (EditorNodeFactory enf : importNodeFactories) {
enf.createNodes(model, importAnnotator);
}
model
.registerEditorNodeWithGraph(new AssetsNode(model
.generateId(null)));
model.registerEditorNodeWithGraph(new StringsNode(model
.generateId(null)));
importAnnotator.reset();
}
// ----- Import, Load, Save
/**
* Loads data from an EAdventure1.x game file. Saves this as an EAdventure
* 2.x editor file.
*
* @param fin
* old-version file to import from
* @param fout
* target folder to build into
*/
public void loadFromImportFile(File fin, File fout) throws IOException {
logger.info(
"Loading editor model from EAD 1.x import '{}' into '{}'...",
fin, fout);
// clear caches & start timer
clear();
importAnnotator.reset();
long nanos = System.nanoTime();
// start import
saveDir = fout;
ProgressProxy pp = new ProgressProxy(0, 0.5f);
model.updateProgress(0, "Starting import...");
// converter.addProgressListener(pp);
converter.convert(fin.getAbsolutePath(), fout.getAbsolutePath());
model.setEngineModel(converter.getModel());
logger.info("{} chapters in model", model.getEngineModel()
.getChapters().size());
// converter.removeProgressListener(pp);
model.updateProgress(52, "Reading strings and engine properties ...");
loadStringsAndProperties(fout);
// build editor model
logger.info("Model loaded; building graph...");
model
.updateProgress(55,
"Converting engine model into editor model...");
ModelVisitorDriver driver = new ModelVisitorDriver();
driver.visit(model.getEngineModel(), new EditorModelVisitor(), model
.getStringHandler());
model.setRoot(model.getNodeFor(model.getEngineModel()));
// add editor high-level data
model.updateProgress(70, "Creating high-level editor elements...");
createEditorNodes();
writeEngineData(fout, true);
writeEditorNodes(fout);
// index & finish
model.updateProgress(90, "Indexing model ...");
model.getNodeIndex().firstIndexUpdate(model.getGraph().vertexSet());
model.updateProgress(100, "... load complete.");
logger.info("Editor model loaded: {} nodes, {} edges, {} seconds",
new Object[] { model.getGraph().vertexSet().size(),
model.getGraph().edgeSet().size(), time(nanos) });
}
/**
* Loads the editor model. Discards the current editing session. The file
* must have been built with save(). Any presentation-related data should be
* added after this is called, using FileUtils.readEntryFromZip(source, ...)
*
* @param sourceDir
* @throws IOException
*/
public void load(File sourceDir) throws IOException {
logger.info("Loading editor model from project dir '{}'...", sourceDir);
// clear caches & start timer
clear();
long nanos = System.nanoTime();
// read
saveDir = sourceDir;
model.updateProgress(10, "Reading engine model ...");
try {
if (sourceDir.isFile()) {
zipTextFileReader.setBase(saveDir);
zipReader.setPath(saveDir.getAbsolutePath()
+ File.separatorChar);
model.setEngineModel(zipReader.readFullModel());
} else {
reader.setPath(saveDir.getAbsolutePath() + File.separatorChar);
model.setEngineModel(reader.readFullModel());
}
model.updateProgress(52,
"Reading strings and engine properties ...");
loadStringsAndProperties(saveDir);
} catch (Exception e) {
throw new IOException("could not load from " + sourceDir, e);
}
// build editor model
logger.info("Model loaded; building graph...");
model
.updateProgress(55,
"Converting engine model into editor model...");
isLoading = true;
ModelVisitorDriver driver = new ModelVisitorDriver();
driver.visit(model.getEngineModel(), new EditorModelVisitor(), model
.getStringHandler());
isLoading = false;
bumpLastElementNodeId();
model.setRoot(model.getNodeFor(model.getEngineModel()));
// index
model.updateProgress(70, "Indexing model ...");
model.getNodeIndex().firstIndexUpdate(model.getGraph().vertexSet());
model.updateProgress(80, "... load complete.");
// add editor high-level data & finish
model.updateProgress(70, "Creating high-level editor elements...");
readEditorNodes(sourceDir);
bumpLastElementNodeId();
logger.info("Editor model loaded: {} nodes, {} edges, {} seconds",
new Object[] { model.getGraph().vertexSet().size(),
model.getGraph().edgeSet().size(), time(nanos) });
}
private void bumpLastElementNodeId() {
// set next editor-id to higher than current highest
int highestAssigned = model.getNodesById().floorKey(
EditorModelImpl.intermediateIDPoint - 1);
logger.debug("Bumping lastElementId to closest to {}: {}",
new Object[] { EditorModelImpl.intermediateIDPoint - 1,
highestAssigned + 1 });
model.setLastElementNodeId(highestAssigned + 1);
}
/**
* load strings and properties
*/
private void loadStringsAndProperties(File base) {
try {
String strings, properties;
if (base.isFile()) {
strings = FileUtils.loadZipEntryToString(base, stringsFile);
} else {
strings = FileUtils
.loadFileToString(new File(base, stringsFile));
}
// FIXME - only reads the current-language versions
StringsReader sr = new StringsReader(parser);
EditorStringHandler stringHandler = new EditorStringHandler();
stringHandler.setStrings(sr.readStrings(strings));
logger.info("Read {} strings", stringHandler.getStrings().size());
model.setStringHandler(stringHandler);
// stringNode retrievable via m.getNodeFor(m.getStringHandler());
DependencyNode stringsNode = new EngineNode<StringHandler>(model
.generateId(null), stringHandler);
model.getNodesByContent().put(stringHandler, stringsNode);
PropertiesReader pr = new PropertiesReader();
HashMap<String, String> engineProperties = new HashMap<String, String>();
// engineProperties.putAll(pr.readProperties(properties));
logger.info("Read {} engine properties", engineProperties.size());
model.setEngineProperties(engineProperties);
} catch (Exception e) {
logger.error("Could not load strings or properties", e);
}
}
/**
* save strings and properties
*/
private void saveStringsAndProperties(File base) {
try {
ByteArrayOutputStream stringOutput = new ByteArrayOutputStream();
// FIXME - only writes the current-language versions
StringWriter sw = new StringWriter();
sw.write(stringOutput, model.getStringHandler().getStrings());
logger.info("Wrote {} strings", model.getStringHandler()
.getStrings().size());
if (base.isFile()) {
ByteArrayInputStream bis;
bis = new ByteArrayInputStream(stringOutput.toByteArray());
FileUtils.appendEntryToZip(base, stringsFile, bis);
} else {
ByteArrayInputStream bis;
bis = new ByteArrayInputStream(stringOutput.toByteArray());
FileUtils.writeToFile(bis, new File(base, stringsFile));
}
logger.info("Wrote {} engine properties", model
.getEngineProperties().size());
} catch (Exception e) {
logger.error("Could not write strings or properties", e);
}
}
/**
* Saves the editor model. Save will contain a normal EAdModel, plus
* resources, plus editor-specific model nodes. Does not include anything
* presentation- related; that should be appended via
* FileUtils.appendEntryToZip(target, ...)
*
* @param target
* ; if null, previous target is assumed
* @throws IOException
*/
public void save(File target) throws IOException {
long nanos = System.nanoTime();
model.updateProgress(5, "Commencing save ...");
if (target != null && saveDir != target) {
// copy over all resource-files first
model
.updateProgress(10,
"Copying resources to new destination ...");
// works for zip-files as well as for whole folders
FileUtils.copy(saveDir, target);
} else if (target == null && saveDir != null) {
target = saveDir;
} else {
throw new IllegalArgumentException(
"Cannot save() without knowing where!");
}
// write main xml
model.updateProgress(50, "Writing engine model ...");
writeEngineData(target, logger.isDebugEnabled());
// write extra xml file to it
model.updateProgress(80, "Writing editor model ...");
int mappings = 0;
try {
mappings = writeEditorNodes(target);
} catch (IOException ioe) {
logger.error("Could not write editor.xml file to {}", target, ioe);
}
saveDir = target;
model.updateProgress(100, "... save complete.");
logger.info("Wrote editor data from {} to {}: {} total objects,"
+ " {} editor mappings, in {} seconds", new Object[] { saveDir,
target, model.getNodesById().size(), mappings, time(nanos) });
}
/**
* Shows how many seconds an operation takes
*/
private String time(long nanoStart) {
long t = System.nanoTime() - nanoStart;
DecimalFormat df = new DecimalFormat("#,###.000");
return df.format((t / 1000000L) / 1000.0);
}
/**
* Clears all model information
*/
private void clear() {
model.clear();
isLoading = false;
importAnnotator.reset();
}
/**
* Returns a file that is relative to this save-file
*
* @param name
* of file to return, relative to save-file
*/
public File relativeFile(String name) {
if (saveDir.exists() && saveDir.isDirectory()) {
return new File(saveDir, name);
} else {
throw new IllegalArgumentException(
"Nothing loaded, loadRelative not available");
}
}
/**
* Writes the data.xml file, optionally with a human-readable copy. Also
* includes internationalized strings and engine properties
*
* @param dest
* destination file
* @param humanReadable
* whether to create a readable copy
* @return
*/
private void writeEngineData(File dest, boolean humanReadable)
throws IOException {
// data
writer.write((AdventureGame) model.getEngineModel(), dest
.getAbsolutePath(), new JavaTextFileWriter());
if (humanReadable) {
for (File f : dest.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.equals("manifest.xml")
|| name.endsWith(".scene")
|| name.endsWith(".chapter");
}
})) {
DataPrettifier.prettify(f, new File(dest, "pretty-"
+ f.getName()));
}
}
// strings and props
saveStringsAndProperties(dest);
}
public File getSaveDir() {
return saveDir;
}
/**
* Re-issues importer progress updates as own updates
*/
public class ProgressProxy implements
AdventureConverter.ImporterProgressListener {
private int start;
private float factor;
public ProgressProxy(int start, float factor) {
this.start = start;
this.factor = factor;
}
@Override
public void update(int progress, String text) {
model.updateProgress(start + (int) (progress * factor), text);
}
}
private static class ZipTextFileReader implements TextFileReader {
private File zipFile;
public void setBase(File zipFile) {
this.zipFile = zipFile;
}
@Override
public String read(String entryName) {
try {
return FileUtils.loadZipEntryToString(zipFile, entryName);
} catch (IOException ioe) {
logger.warn("Could not read file {}: {}", zipFile + "::"
+ entryName, ioe.toString());
return "ERROR";
}
}
}
}