package sharpen.xobotos.config.xstream; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import sharpen.core.Sharpen; import sharpen.xobotos.config.ConfigurationException; import sharpen.xobotos.config.annotations.AttributeReference; import sharpen.xobotos.config.annotations.ReadIncludeFile; import sharpen.xobotos.config.annotations.ReferenceById; import sharpen.xobotos.config.annotations.RootContextReference; import java.io.File; import java.lang.reflect.Field; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Stack; import java.util.logging.Level; public final class RootContext<T extends IConfigurationFile> { private final URI _projectRoot; private final Class<T> _klass; private final MarshallingStrategy _strategy; private final Stack<Scope> _stack; private final Scope _rootScope; private final Environment _environment; private final List<ReferenceEntry> _referenceEntries; public URI getProjectRoot() { return _projectRoot; } public String getProjectPath(String fileName) { return _projectRoot.getPath() + File.separatorChar + fileName; } protected MarshallingStrategy getMarshallingStrategy() { return _strategy; } private RootContext(Environment environment, URI projectRoot, String fileName, Class<T> klass) { this._environment = environment; this._projectRoot = projectRoot; this._klass = klass; this._strategy = new MarshallingStrategy(this); _referenceEntries = new ArrayList<ReferenceEntry>(); _stack = new Stack<Scope>(); _rootScope = new Scope(null, null, fileName); _stack.push(_rootScope); } public <U> U my(Class<U> klass) { return _environment.provide(klass); } public static <U extends IConfigurationFile> U readConfigurationFile(URI projectRoot, String fileName, Class<U> klass, Environment environment) { return new RootContext<U>(environment, projectRoot, fileName, klass).read(); } public static <U extends IConfigurationFile> U readConfigurationFile(URI projectRoot, String fileName, Class<U> klass, Object... variables) { Environment environment = new Environment(variables); return new RootContext<U>(environment, projectRoot, fileName, klass).read(); } private T read() { T result = readFragment(_rootScope.getFileName(), _klass); resolveReferences(); _environment.add(result); return result; } protected <U extends IConfigurationFragment> U readFragment(String fileName, Class<U> klass) { return XStreamUtils.readFragment(this, fileName, klass); } private ReferenceById getAnnotation(Class<?> klass) { while (klass != null) { ReferenceById annotation = klass.getAnnotation(ReferenceById.class); if (annotation != null) return annotation; klass = klass.getSuperclass(); } return null; } private boolean hasAnnotation(Class<?> klass) { return getAnnotation(klass) != null; } protected Scope enterScope(Class<?> klass, String key) { if (!hasAnnotation(klass) || (key == null) || key.isEmpty()) return null; Scope scope = new Scope(_stack.peek(), key, null); _stack.push(scope); return scope; } protected void leaveScope(Scope scope, Object parent, Class<?> klass, Object result) { if (result != null) postProcess(scope, parent, klass, result); if (scope != null) { _stack.peek().assign(result); _stack.pop(); } } private Scope getRootScope() { return _rootScope; } private Scope getCurrentScope() { return _stack.peek(); } private Scope getFileScope() { Scope scope = _stack.peek(); while (!scope.isRoot() && !scope.parent().isFileScope()) scope = scope.parent(); return scope; } protected Object lookupReference(String reference) { Scope scope; if (reference.startsWith("/")) { scope = getRootScope(); reference = reference.substring(1); } else if (reference.startsWith("$")) { scope = getCurrentScope(); reference = reference.substring(1); } else scope = getFileScope(); Object value = scope.lookup(reference); if (value == null) { Sharpen.Log(Level.SEVERE, "[%s]: invalid reference '%s'", scope, reference); throw new ConfigurationException("Invalid reference '%s'", reference); } return value; } static final class Scope { private final Scope _parent; private final String _name; private Object _value; private Map<String, Scope> _children = new HashMap<String, Scope>(); private List<Scope> _fileScopes = new ArrayList<Scope>(); private final String _fileName; public Scope parent() { return _parent; } public String name() { return _name; } public String getFileName() { return _fileName; } public boolean isRoot() { return _parent == null; } public boolean isFileScope() { return _fileName != null; } public Object value() { return _value; } public boolean containsKey(String key) { return _children.containsKey(key); } public Scope get(String key) { return _children.get(key); } protected void addChild(Scope child) { if (child.isFileScope()) _fileScopes.add(child); else _children.put(child.name(), child); } protected void assign(Object value) { this._value = value; } public String getPath() { if (_parent == null) return "/"; if (_fileName != null) return _parent.getPath(); return _parent.getPath() + "/" + _name; } private Scope lookupScope(String key) { if (key.equals("..")) return _parent; if (_children.containsKey(key)) return _children.get(key); for (final Scope child : _fileScopes) { if (child.containsKey(key)) return child.get(key); } return null; } protected Object lookup(String reference) { String key; int pos = reference.indexOf('/'); if (pos < 0) key = reference; else key = reference.substring(0, pos); Scope scope = lookupScope(key); if (scope == null) return null; if (pos < 0) return scope.value(); String rest = reference.substring(pos + 1); return scope.lookup(rest); } protected Scope(Scope parent, String name, String fileName) { this._parent = parent; this._name = name; this._fileName = fileName; if (parent != null) parent.addChild(this); } @Override public String toString() { if (_parent == null) return String.format("RootScope[/:%s]", _fileName); else if (_fileName != null) return String.format("FileScope[%s:%s]", getPath(), _fileName); return String.format("Scope[%s:%s]", getPath(), _value != null ? _value.toString() : "null"); } } private void postProcess(Scope scope, Object parent, Class<?> klass, Object result) { checkIncludeFile(klass, result); setRootContextField(klass, result); if (scope != null) setIdField(klass, result); } private void checkIncludeFile(Class<?> klass, Object obj) { while (klass != null) { ReadIncludeFile annotation = klass.getAnnotation(ReadIncludeFile.class); if (annotation == null) { klass = klass.getSuperclass(); continue; } processIncludeFile(klass, annotation, obj); return; } } private void setRootContextField(Class<?> klass, Object obj) { while (klass != null) { RootContextReference annotation = klass.getAnnotation(RootContextReference.class); if (annotation == null) { klass = klass.getSuperclass(); continue; } final Field field; try { field = klass.getDeclaredField(annotation.value()); field.setAccessible(true); field.set(obj, this); } catch (Exception e) { throw new ConfigurationException("Cannot set root context field in '%s'", klass); } return; } } private void setIdField(Class<?> klass, Object obj) { while (klass != null) { ReferenceById annotation = klass.getAnnotation(ReferenceById.class); if (annotation == null) { klass = klass.getSuperclass(); continue; } final String path = buildPath(); final Field field; try { field = klass.getDeclaredField(annotation.value()); field.setAccessible(true); field.set(obj, path); } catch (Exception e) { throw new ConfigurationException("Cannot set ID field in '%s'", klass); } return; } } private final static class ReferenceEntry { public final String attribute; public final Field field; public final Object instance; public ReferenceEntry(String attr, Field field, Object instance) { this.attribute = attr; this.field = field; this.instance = instance; } } protected void checkAttributeReferences(HierarchicalStreamReader reader, Scope scope, Class<?> klass, Object obj) { while (klass != null) { for (final Field field : klass.getDeclaredFields()) { AttributeReference annotation = field.getAnnotation(AttributeReference.class); if (annotation == null) continue; String attr = reader.getAttribute(annotation.value()); if (attr == null) continue; if (attr.startsWith("/")) { _referenceEntries.add(new ReferenceEntry(attr, field, obj)); continue; } Object reference = lookupReference(attr); try { field.setAccessible(true); field.set(obj, reference); } catch (Exception e) { throw new ConfigurationException("Cannot set field '%s'", field); } } klass = klass.getSuperclass(); } } private void resolveReferences() { for (final ReferenceEntry entry : _referenceEntries) { Object reference = _rootScope.lookup(entry.attribute.substring(1)); if (reference == null) { _rootScope.lookup(entry.attribute.substring(1)); Sharpen.Log(Level.SEVERE, "Invalid reference '%s'", entry.attribute); throw new ConfigurationException("Invalid reference '%s'", entry.attribute); } try { entry.field.setAccessible(true); entry.field.set(entry.instance, reference); } catch (Exception e) { throw new ConfigurationException("Cannot set field '%s'", entry.field); } } } private String buildPath() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < _stack.size(); i++) { Scope scope = _stack.get(i); if (scope.name() == null) continue; if (sb.length() > 0) sb.append('/'); sb.append(scope.name()); } return sb.toString(); } private void processIncludeFile(Class<?> klass, ReadIncludeFile annotation, Object include) { final String fileName; try { Field field = klass.getDeclaredField(annotation.fileNameField()); field.setAccessible(true); fileName = (String)field.get(include); } catch (Exception e) { throw new ConfigurationException("Cannot get include file name in '%s'", klass); } for (final Scope scope : _stack) { if (scope.isFileScope() && scope.getFileName().equals(fileName)) throw new ConfigurationException("Duplicate include file '%s'", fileName); } _stack.push(new Scope(_stack.peek(), null, fileName)); IConfigurationFile contents = XStreamUtils.readFragment(this, fileName, annotation.fileType()); _stack.pop(); try { Field field = klass.getDeclaredField(annotation.contentsField()); field.setAccessible(true); field.set(include, contents); } catch (Exception e) { throw new ConfigurationException("Cannot set include file contents field in '%s'", klass); } } }