/**
* 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.DependencyEdge;
import es.eucm.ead.editor.model.nodes.DependencyNode;
import es.eucm.ead.editor.model.nodes.EditorNode;
import es.eucm.ead.editor.model.nodes.EngineNode;
import es.eucm.ead.editor.model.visitor.ModelVisitor;
import es.eucm.ead.editor.model.visitor.ModelVisitorDriver;
import es.eucm.ead.model.elements.AdventureGame;
import es.eucm.ead.model.interfaces.features.Identified;
import org.jgrapht.graph.ListenableDirectedGraph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Contains a full model of what is being edited. This is a super-set of an
* AdventureGame, encompassing both engine-related model objects and
* resources, assets, and strings. Everything is searchable, and dependencies
* are tracked as objects are changed.
*
* @author mfreire
*/
public class EditorModelImpl implements EditorModel {
private static Logger logger = LoggerFactory
.getLogger(EditorModelImpl.class);
/**
* A large number, hard to reach counting from 0 upwards
*/
public static final int intermediateIDPoint = 1 << 24;
/**
* Id used for 'bad' elements; warnings will be shown if forced to used it
*/
public static final int badElementId = -1;
/**
* Node id generation: default ids
*/
private int lastElementNodeId = 0;
/**
* Node id generation: transients. Not serialized on save/load.
* Should never touch default ids.
*/
private int lastTransientNodeId = intermediateIDPoint;
/**
* Dependency graph; main model structure
*/
private ListenableDirectedGraph<DependencyNode, DependencyEdge> g;
/**
* Quick reference for node retrieval; uses editor-ids.
*/
private TreeMap<Integer, DependencyNode> nodesById;
/**
* Quick reference for node retrieval; uses contents. Available only
* for content-types that do not have embedded editor-ids
*/
private HashMap<Object, DependencyNode> nodesByContent;
/**
* The root of the graph; contains the engineModel
*/
private DependencyNode root;
/**
* Internationalized strings
*/
private EditorStringHandler stringHandler;
/**
* Game properties. Warning: does not preserve comments
*/
private HashMap<String, String> engineProperties;
/**
* Search index
*/
private ModelIndex nodeIndex;
/**
* Used to quickly search editor-nodes for editor-ids
*/
public static final Pattern editorIdPattern = Pattern
.compile("__([0-9]+)__.*");
/**
* Engine model
*/
private AdventureGame engineModel;
/**
* Loader - in charge of save/load operations
*/
private EditorModelLoader loader;
/**
* Listeners for long operations
*/
private ArrayList<ModelProgressListener> progressListeners = new ArrayList<ModelProgressListener>();
/**
* Listeners for model changes
*/
private ArrayList<ModelListener> modelListeners = new ArrayList<ModelListener>();
/**
* Constructor. Does not do much beyond initializing fields.
*
* @param reader
* @param importer
* @param writer
*/
@Inject
public EditorModelImpl(EditorModelLoader loader) {
g = new ListenableDirectedGraph<DependencyNode, DependencyEdge>(
DependencyEdge.class);
this.nodesById = new TreeMap<Integer, DependencyNode>();
this.nodesByContent = new HashMap<Object, DependencyNode>();
this.nodeIndex = new ModelIndex();
this.loader = loader;
}
// ----- nodes
@Override
public DependencyNode getNode(int id) {
return nodesById.get(id);
}
/**
* Registers a dependencyNode, launching the appropriate event to inform
* listeners. The node must not depend upon others (or have all dependencies
* already wired-in). This is ideal for editor-windows that are not actually
* tied into the current model.
*
* @param n the node
* @param eventType the event-type
*/
public void registerNode(DependencyNode n, String eventType) {
nodesById.put(n.getId(), n);
fireModelEvent(new DefaultModelEvent(eventType, this,
new DependencyNode[] { n }, null));
}
/**
* Gets a unique ID. All new DependencyNodes should get their IDs this way.
* Uses a static field to store the last assigned ID; standard disclaimers
* on thread-safety and class-loaders apply.
*
* @param targetObject
* an engine object, or null for synthetic elements
* @return
*/
@Override
public int generateId(Object targetObject) {
int assigned = (targetObject == null || targetObject instanceof Identified) ? lastElementNodeId++
: lastTransientNodeId++;
if (nodesById.containsKey(assigned)) {
logger.error("Duplicate ID {} for object {} (was {})",
new Object[] { assigned, targetObject,
nodesById.get(assigned) });
// will keep on trying until it finds a free id
return generateId(targetObject);
}
return assigned;
}
/**
* Makes sure that the returned id contains an eid-prefix.
*
* @see createOrUnfreeze for details
* @param id
* to alter
* @param eid
* to insert (not inserted if already present)
* @return the (possibly-altered) eid
*/
public static String decorateIdWithEid(String id, int eid) {
if (id == null) {
return "__" + eid + "__";
}
Matcher m = editorIdPattern.matcher(id);
if (m.find() && m.group(1).equals("" + eid)) {
return id;
} else {
return "__" + eid + "__" + id;
}
}
/**
* Returns the editor-id of the object
* @param o
* @return editorId if the object has a valid editorId (Identified or
* the StringHandler) - or badElementId otherwise.
*/
@Override
public int getEditorId(Object o) {
if (o instanceof Identified) {
Identified i = (Identified) o;
if (i.getId() != null) {
Matcher m = editorIdPattern.matcher(i.getId());
if (m.find()) {
return Integer.parseInt(m.group(1));
}
} else {
logger.warn("null ID for {}; some kind of ID was expected", o
.toString());
}
}
return badElementId;
}
/**
* Returns the DependencyNode that wraps a given content.
* @param content to locate
* @return the DependencyNode for an object that is wrapped in an editorNode.
* This works in two ways. First, if it has an editor-id tag, it is used.
* Otherwise, it must have been an unmarked object (list, map, resource, ...);
* and the transientCollection-->editorNode map is used instead.
*/
@Override
public DependencyNode getNodeFor(Object content) {
int eid = getEditorId(content);
if (eid < 0) {
return nodesByContent.get(content);
} else {
return nodesById.get(eid);
}
}
@Override
public List<DependencyNode> incomingDependencies(DependencyNode node) {
ArrayList<DependencyNode> ns = new ArrayList<DependencyNode>();
for (DependencyEdge e : g.incomingEdgesOf(node)) {
ns.add(g.getEdgeSource(e));
}
return ns;
}
@Override
public List<DependencyNode> outgoingDependencies(DependencyNode node) {
ArrayList<DependencyNode> ns = new ArrayList<DependencyNode>();
for (DependencyEdge e : g.outgoingEdgesOf(node)) {
ns.add(g.getEdgeTarget(e));
}
return ns;
}
@Override
public void updateDependencies(Set<DependencyNode> changed,
Set<DependencyNode> added, DependencyNode... nodes) {
ModelVisitorDriver mvd = new ModelVisitorDriver();
// first pass
for (DependencyNode node : nodes) {
// make copies of old dependencies
HashSet<DependencyEdge> oldIncoming = new HashSet<DependencyEdge>(g
.incomingEdgesOf(node));
HashSet<DependencyEdge> oldOutgoing = new HashSet<DependencyEdge>(g
.outgoingEdgesOf(node));
// update incoming
for (DependencyEdge e : oldIncoming) {
LinkStillThereVisitor lstv = new LinkStillThereVisitor(e);
mvd.visit(g.getEdgeSource(e), lstv, stringHandler);
if (!lstv.isEdgeStillThere()) {
changed.add(g.getEdgeSource(e));
g.removeEdge(e);
}
}
// remove old-outgoing
for (DependencyEdge e : oldOutgoing) {
g.removeEdge(e);
}
}
// second pass
for (DependencyNode node : nodes) {
// add new-outgoing
if (node instanceof EngineNode) {
UpdateOutgoingLinksVisitor uolv = new UpdateOutgoingLinksVisitor(
node, added, changed);
mvd.visit(node, uolv, stringHandler);
} else if (node instanceof EditorNode) {
// editor nodes have no outgoing to worry about
} else {
logger.error("Expected editorNode or engineNode; "
+ "{} (id {}) is neither", node.getClass(), node
.getId());
}
}
}
private class LinkStillThereVisitor implements ModelVisitor {
private final int sourceId;
private final int targetId;
private final String type;
private boolean found = false;
public LinkStillThereVisitor(DependencyEdge e) {
this.sourceId = g.getEdgeSource(e).getId();
this.targetId = g.getEdgeTarget(e).getId();
this.type = e.getType();
}
@Override
public boolean visitObject(Object target, Object source,
String sourceName) {
if (!found && sourceName.equals(this.type)) {
DependencyNode sourceNode = getNodeFor(source);
DependencyNode targetNode = getNodeFor(target);
if (sourceNode != null && targetNode != null
&& sourceNode.getId() == sourceId
&& targetNode.getId() == targetId) {
found = true;
}
}
// never recurse
return false;
}
public boolean isEdgeStillThere() {
return found;
}
@Override
public void visitProperty(Object target, String propertyName,
String textValue) {
// not interested in indexing properties; the index takes care of itself
}
}
private class UpdateOutgoingLinksVisitor implements ModelVisitor {
private final Set<DependencyNode> changed;
private final Set<DependencyNode> added;
private final DependencyNode sourceNode;
private UpdateOutgoingLinksVisitor(DependencyNode sourceNode,
Set<DependencyNode> added, Set<DependencyNode> changed) {
this.sourceNode = sourceNode;
this.changed = changed;
this.added = added;
}
@Override
public boolean visitObject(Object target, Object source,
String sourceName) {
Set<DependencyEdge> edges = g.getAllEdges(getNodeFor(source),
getNodeFor(target));
boolean found = false;
for (DependencyEdge e : edges) {
if (e.getType().equals(sourceName)) {
found = true;
break;
}
}
if (!found) {
// add edge, and possibly target node
DependencyNode targetNode = addNode(sourceNode, sourceName,
target, false);
if (targetNode != null) {
// new node! keep on going, then!
added.add(targetNode);
return true;
} else {
changed.add(targetNode);
return false;
}
} else {
// found: do not recurse
return false;
}
}
@Override
public void visitProperty(Object target, String propertyName,
String textValue) {
// not interested in indexing properties; the index takes care of itself
}
}
/**
* Attempts to add a new node-and-edge to the graph.
* The source may be null (for the root).
*
* @param source node (null if root)
* @param type textual name of edge
* @param targetContent to wrap in a node if applicable
* @param isLoading to distinguish between load & create
* @return the new node if added, or null if already existing (and
* therefore, it makes no sense to continue adding recursively from
* there on).
*/
public DependencyNode addNode(DependencyNode source, String type,
Object targetContent, boolean isLoading) {
DependencyNode target = getNodeFor(targetContent);
boolean alreadyKnown = (target != null);
if (!alreadyKnown) {
target = createOrUnfreezeNode(targetContent, isLoading);
getGraph().addVertex(target);
}
if (source != null) {
getGraph().addEdge(source, target, new DependencyEdge(type));
} else {
setRoot(target);
}
if (!alreadyKnown) {
return target;
} else {
return null;
}
}
/**
* Wraps a targetContent in an DependencyNode. If the content is of a type
* that has extra editor data associated (a subclass of Identified), and
* this editor data is available, it is used; otherwise, a new
* DependencyNode is created.
*
* @param targetContent to wrap
* @return a new or old editorNode to wrap that content
*/
@SuppressWarnings("unchecked")
private DependencyNode createOrUnfreezeNode(Object targetContent,
boolean isLoading) {
DependencyNode node;
if (targetContent instanceof Identified) {
String oid = ((Identified) targetContent).getId();
int eid = getEditorId(targetContent);
if (eid != EditorModelImpl.badElementId) {
// content is eadElement, and has editor-id: unfreeze
logger.debug("Found existing eID marker in {}: {}",
targetContent.getClass().getSimpleName(), oid);
node = getNodesById().get(eid);
if (node == null) {
node = new EngineNode(eid, targetContent);
getNodesById().put(eid, node);
} else {
if (!node.getContent().equals(targetContent)) {
logger
.error(
"Corrupted save-file: eid {} assigned to {} AND {}",
new Object[] { eid,
targetContent.toString(),
node.getContent().toString() });
throw new IllegalStateException("Corrupted save-file: "
+ "same eid assigned to two objects");
}
}
} else {
// content is eadElement, but has no editor-id: add it
if (isLoading) {
logger
.error(
"Loaded BasicElement {} of type {} had no editor ID",
oid, targetContent.getClass()
.getSimpleName());
throw new IllegalStateException("Corrupted save-file: "
+ "no eid assigned to loaded objects");
} else {
eid = generateId(targetContent);
String decorated = EditorModelImpl.decorateIdWithEid(oid,
eid);
logger.debug("Created eID marker for {}: {} ({})",
new Object[] { oid, eid, decorated });
((Identified) targetContent).setId(decorated);
node = new EngineNode(eid, targetContent);
getNodesById().put(eid, node);
}
}
} else {
logger.error(
"Tried to wrap non-Identified of type {} in an editorID",
targetContent.getClass().getSimpleName());
throw new IllegalStateException("Cannot continue load or import");
}
return node;
}
// ----- Internal access (mostly for loading & saving)
/**
* Flushes the model.
*/
public void clear() {
lastElementNodeId = 0;
lastTransientNodeId = intermediateIDPoint;
nodesById.clear();
nodesByContent.clear();
nodeIndex.clear();
g.removeAllEdges(new HashSet<DependencyEdge>(g.edgeSet()));
g.removeAllVertices(new HashSet<DependencyNode>(g.vertexSet()));
g = new ListenableDirectedGraph<DependencyNode, DependencyEdge>(
DependencyEdge.class);
}
public ListenableDirectedGraph<DependencyNode, DependencyEdge> getGraph() {
return g;
}
public DependencyNode getRoot() {
return root;
}
public void setRoot(DependencyNode root) {
this.root = root;
}
public void setLastElementNodeId(int lastElementNodeId) {
this.lastElementNodeId = lastElementNodeId;
}
public void setEngineModel(AdventureGame engineModel) {
this.engineModel = engineModel;
}
public TreeMap<Integer, DependencyNode> getNodesById() {
return nodesById;
}
public HashMap<Object, DependencyNode> getNodesByContent() {
return nodesByContent;
}
public ModelIndex getNodeIndex() {
return nodeIndex;
}
public File getResourcePath() {
return loader.getSaveDir();
}
public void setStringHandler(EditorStringHandler stringHandler) {
this.stringHandler = stringHandler;
}
public void setEngineProperties(HashMap<String, String> engineProperties) {
this.engineProperties = engineProperties;
}
// ----- ModelAccessor
/**
* Gets the model element with id 'id'. Also generates synthetic nodes
* on-demand
* @throws NoSuchElementException if not found.
* @param id of element (assigned by editor when project is imported)
* @return element with id as its editor-id
*/
@Override
public DependencyNode getElement(String id) {
if (id == null || id.isEmpty()) {
return null;
}
int eid = Integer.parseInt(id);
return getNode(eid);
}
@Override
public DependencyNode createElement(Class<? extends DependencyNode> type) {
DependencyNode node = null;
try {
Constructor c = type.getConstructor(Integer.TYPE);
node = (DependencyNode) c.newInstance(generateId(null));
} catch (Exception e) {
logger.error("Cannot create EditorNode of class {}",
type.getName(), e);
}
return node;
}
@Override
public DependencyNode copyElement(DependencyNode e) {
throw new UnsupportedOperationException("Not yet supported");
}
// ----- EditorNode manipulation
/**
* Adds a new EditorNode to the dependency-tracking graph
* @param e the node to register
*/
public void registerEditorNodeWithGraph(EditorNode e) {
nodesById.put(e.getId(), e);
logger.debug("registering {}", e.getTextualDescription(this));
g.addVertex(e);
for (DependencyNode n : e.getContents()) {
logger.debug("\ttarget is {}", n.getTextualDescription(this));
g.addEdge(e, n, new DependencyEdge(e.getClass().getName()));
}
}
/**
* Replaces a node for another node. Incoming and outgoing references are
* retained;
* @param n
* @param replacement
*/
private void replaceVertex(DependencyNode n, DependencyNode replacement) {
ArrayList<DependencyEdge> es = new ArrayList<DependencyEdge>();
es.addAll(g.edgesOf(n));
for (DependencyEdge e : es) {
logger.debug("Fixing up edge {} ---[{}]---> {}");
DependencyNode source = g.getEdgeSource(e);
DependencyNode target = g.getEdgeTarget(e);
if (source == n) {
source = replacement;
} else {
target = replacement;
}
g.addEdge(source, target, new DependencyEdge(e.getType()));
}
g.removeAllEdges(es);
}
// -------- saving, loading, and engine access
@Override
public EditorModelLoader getLoader() {
loader.setModel(this);
nodeIndex.setModel(this);
return loader;
}
@Override
public AdventureGame getEngineModel() {
return engineModel;
}
@Override
public EditorStringHandler getStringHandler() {
return stringHandler;
}
@Override
public HashMap<String, String> getEngineProperties() {
return engineProperties;
}
// ---- search-related functions API ----
/**
* Queries all fields in all nodes for the provided text. This
* variant provides details of any matches.
*
* @param query
* @return a list of all matching nodes, ranked by relevance
*/
@Override
public ModelIndex.SearchResult search(ModelQuery query) {
return nodeIndex.search(query);
}
/**
* Retrieves a list of all indexed fields.
* @return a list of all indexed fields.
*/
public List<String> getAllSearchableFields() {
return nodeIndex.getIndexedFieldNames();
}
// ----- progress -----
@Override
public void addProgressListener(ModelProgressListener progressListener) {
progressListeners.add(progressListener);
}
@Override
public void removeProgressListener(ModelProgressListener progressListener) {
progressListeners.remove(progressListener);
}
public void updateProgress(int progress, String text) {
logger.debug("Model progress update: {}", text);
for (ModelProgressListener l : progressListeners) {
l.update(progress, text);
}
}
// ----- progress -----
@Override
public void addModelListener(ModelListener modelListener) {
logger.info("--> [+] registered new ModelListener {}", modelListener);
modelListeners.add(modelListener);
}
@Override
public void removeModelListener(ModelListener modelListener) {
logger.info("--> [-] removed ModelListener {}", modelListener);
modelListeners.remove(modelListener);
}
@Override
public void fireModelEvent(ModelEvent event) {
logger.info("{} listeners for model-event: {}", new Object[] {
modelListeners.size(), event });
for (ModelListener l : modelListeners) {
logger.info("--> now delivering to {}", l);
l.modelChanged(event);
}
}
}