/**
* 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.visitor;
import es.eucm.ead.editor.EditorStringHandler;
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.model.assets.AssetDescriptor;
import es.eucm.ead.model.elements.AdventureGame;
import es.eucm.ead.model.elements.BasicElement;
import es.eucm.ead.model.elements.Chapter;
import es.eucm.ead.model.elements.extra.EAdList;
import es.eucm.ead.model.elements.extra.EAdMap;
import es.eucm.ead.model.interfaces.Param;
import es.eucm.ead.model.params.EAdParam;
import es.eucm.ead.model.params.text.EAdString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Visits parts of the model. Given a ModelVisitor and a start, the visitor
* is driven throught the model in a recursive depth-first fashion. Visitors
* are expected to say when to stop, and to take notes of anything they like
* during the way. They will be called once per visited node, and once per
* discovered searchable property.
*
* @author mfreire
*/
public class ModelVisitorDriver {
private static final Logger logger = LoggerFactory
.getLogger(ModelVisitorDriver.class);
// drivers are reusable; they keep no internal information
private ElementDriver elementDriver = new ElementDriver();
private ListDriver listDriver = new ListDriver();
private MapDriver mapDriver = new MapDriver();
private ParamDriver paramDriver = new ParamDriver();
private AssetDriver assetDriver = new AssetDriver();
private EditorNodeDriver editorNodeDriver = new EditorNodeDriver();
private EngineNodeDriver engineNodeDriver = new EngineNodeDriver();
private EditorStringHandler esh;
private ModelVisitor v = null;
/**
* Visits all elements in data in a depth-first manner. Does not keep track
* of duplicate EAdElements (the visitor should do that). Notifies the
* visitor of all elements, their properties, and their assets.
*
* @param data model to visit
* @param v visitor
*/
public void visit(AdventureGame data, ModelVisitor v,
EditorStringHandler esh) {
this.v = v;
this.esh = esh;
try {
// visit the root element, and continue from there
driveInto(data, null, null);
} catch (Exception e) {
logger.error("Error visiting model", e);
}
}
/**
* Re-visit a part of the model. The visitor should not return 'true'
* visitObject queries which should not be pursued.
* @param o
* @param v
* @param esh
*/
public void visit(DependencyNode node, ModelVisitor v,
EditorStringHandler esh) {
this.v = v;
this.esh = esh;
try {
// visit the initial node;
// - if EditorNode, then visit only properties
// - if general
driveInto(node, null, null);
} catch (Exception e) {
logger.error("Error visiting model", e);
}
}
/**
* Returns the correct driver to use for a given object.
* @param o object to drive into.
* @return
*/
@SuppressWarnings("unchecked")
private VisitorDriver driverFor(Object o) {
if (o instanceof EditorNode) {
return editorNodeDriver;
} else if (o instanceof EngineNode) {
return engineNodeDriver;
} else if (o instanceof BasicElement) {
return elementDriver;
} else if (o instanceof EAdList) {
return listDriver;
} else if (o instanceof EAdMap) {
return mapDriver;
} else if (o instanceof EAdParam) {
return paramDriver;
} else if (o instanceof AssetDescriptor) {
return assetDriver;
} else {
return paramDriver;
}
}
/**
* Deepens visit into object o; called by drivers.
*/
@SuppressWarnings("unchecked")
public void driveInto(Object o, Object source, String sourceName) {
logger.debug("Driving into target of type {} [{}]", o.getClass()
.getName(), (o instanceof BasicElement ? ((BasicElement) o)
.getId() : o.getClass().getSimpleName() + "@" + o.hashCode()));
if (v == null) {
throw new IllegalStateException("No visitor defined. End of visit.");
}
VisitorDriver d = driverFor(o);
if (d instanceof ParamDriver || d instanceof MapDriver
|| d instanceof ListDriver) {
logger.debug("auto-driving into {} of type {} with a {}",
new Object[] { o.toString(), o.getClass().getSimpleName(),
d.getClass().getSimpleName() });
d.drive(o, source, sourceName);
} else if (v.visitObject(o, source, sourceName)) {
logger.debug("driving into {} of type {} with a {}", new Object[] {
o.toString(), o.getClass().getSimpleName(),
d.getClass().getSimpleName() });
d.drive(o, source, sourceName);
}
}
/**
* visits a DependencyNode.
*/
private class EngineNodeDriver implements VisitorDriver<EngineNode> {
@Override
@SuppressWarnings("unchecked")
public void drive(EngineNode target, Object source, String sourceName) {
Object o = target.getContent();
driverFor(o).drive(o, target, "content");
}
}
/**
* visits an EditorNode.
*/
private class EditorNodeDriver implements VisitorDriver<EditorNode> {
@Override
public void drive(EditorNode target, Object source, String sourceName) {
logger.info("Driving into an EditorNode! Wow!");
for (String f : target.getIndexedFields()) {
Object o = readProperty(target, f);
if (!isEmpty(o)) {
logger.info("\t'{}' has a '{}' property!", new Object[] {
target, f });
if (o instanceof List) {
driveInto(o, target, f);
} else {
v.visitProperty(target, f, o.toString());
}
}
}
}
}
/**
* visits an EAdElement.
*/
private class ElementDriver implements VisitorDriver<BasicElement> {
@Override
public void drive(BasicElement target, Object source, String sourceName) {
processParams(target);
if (target instanceof AdventureGame) {
processParam(target, "chapters");
processParam(target, "initialChapter");
} else if (target instanceof Chapter) {
processParam(target, "scenes");
processParam(target, "initialScene");
}
}
}
public static final String listSuffix = "-list";
/**
* visits a list - either by adding it all as attributes, or some other
* method.
*/
private class ListDriver implements VisitorDriver<EAdList<?>> {
@Override
public void drive(EAdList<?> target, Object source, String sourceName) {
for (int i = 0; i < target.size(); i++) {
// visit all children-values of this list
Object o = target.get(i);
if (o != null) {
driveInto(o, source, sourceName + listSuffix);
}
}
}
}
public static final String mapKeySuffix = "-map-key";
public static final String mapValueSuffix = "-map-value";
/**
* visits a map (keys and values).
*/
private class MapDriver implements VisitorDriver<EAdMap<?>> {
@Override
public void drive(EAdMap<?> target, Object source, String sourceName) {
int i = 0;
for (Map.Entry<?, ?> e : target.entrySet()) {
if (e.getKey() != null && e.getValue() != null) {
driveInto(e.getKey(), source, sourceName + mapKeySuffix);
driveInto(e.getValue(), source, sourceName + mapValueSuffix);
}
i++;
}
}
}
/**
* visits a param - or any kind of field storable as an XML attribute.
* Will only be called if this is not an element, or a list/map
*/
private class ParamDriver implements VisitorDriver<Object> {
@Override
public void drive(Object target, Object source, String sourceName) {
if (target == null) {
logger.warn("Null data");
} else {
if (target instanceof EAdString) {
String value = esh.getString((EAdString) target);
v.visitProperty(source, sourceName, value);
} else if (target instanceof Class) {
String value = ((Class<?>) target).getName();
v.visitProperty(source, sourceName, value);
} else if (target instanceof EAdParam) {
String value = ((EAdParam) target).toStringData();
v.visitProperty(source, sourceName, value);
} else {
v.visitProperty(source, sourceName, target.toString());
}
}
}
}
public static final String resourceAssetSuffix = "-inner-asset";
public static final String resourceBundleSuffix = "-inner-bundle-id";
/**
* visits an asset.
*/
private class AssetDriver implements VisitorDriver<AssetDescriptor> {
@Override
public void drive(AssetDescriptor target, Object source,
String sourceName) {
processParams(target);
}
}
/**
* Process field parameters; called by drivers
*
* @param data object where parameters are to be processed
* @param v
*/
private void processParams(Object data) {
logger.debug("Iterating properties of {}", data);
Class<?> clazz = data.getClass();
while (clazz != null) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
try {
Param param = field.getAnnotation(Param.class);
String fieldName = field.getName();
if (param != null) {
processParam(data, fieldName);
}
} catch (Exception e) {
logger.error("Could not access properties of {}, from {}",
new Object[] { clazz, data.getClass(), e });
}
}
clazz = clazz.getSuperclass();
}
logger.debug("Finished properties of {}", data);
}
private void processParam(Object data, String fieldName) {
Object o = readProperty(data, fieldName);
if (!isEmpty(o)) {
logger.debug("\t'{}' has a '{}' property!", new Object[] { data,
fieldName });
driveInto(o, data, fieldName);
}
}
/**
* Utility method to find a property descriptor for a single property
*
* @param object to look into
* @param fieldName to find within object
* @return
*/
public static Object readProperty(Object object, String fieldName) {
Class<?> c = object.getClass();
try {
PropertyDescriptor pd = null;
for (PropertyDescriptor d : Introspector.getBeanInfo(c)
.getPropertyDescriptors()) {
if (d.getName().equals(fieldName)) {
pd = d;
}
}
if (pd == null) {
logger.error("Missing descriptor for {} -- trace follows", c
+ "." + fieldName, new Exception());
return null;
}
Method method = pd.getReadMethod();
if (method == null) {
logger.error("Missing read-method for {} in {} ", fieldName, c);
logger.error("Read-method fail from ", new Exception());
return null;
}
logger.debug("\t invoking {}", fieldName);
return method.invoke(object);
} catch (Exception e) {
logger.error("Error calling getter for " + "field {} in class {} ",
new Object[] { fieldName, c }, e);
return null;
}
}
/**
* Determines if an object is null or empty. This includes empty maps or
* lists
*
* @param o the object to check
* @return
*/
public static boolean isEmpty(Object o) {
return ((o == null)
|| (o instanceof Collection && ((Collection<?>) o).isEmpty())
|| (o instanceof EAdList && ((EAdList<?>) o).size() == 0) || (o instanceof EAdMap && ((EAdMap<?>) o)
.isEmpty()));
}
/**
* Implemented by actual drivers
*
* @param <T>
*/
private interface VisitorDriver<T> {
/**
* Drive to another node
*
* @param target the target node
* @param source the source node; only EAdElements can serve as sources
* @param sourceName the name of the property within the source that we
* are acting on
*/
public abstract void drive(T target, Object source, String sourceName);
}
}