1package example.xml;
2
3import com.google.inject.Key;
4import com.google.inject.Module;
5import com.google.inject.Provider;
6import com.google.inject.Binder;
7import java.io.InputStreamReader;
8import java.io.Reader;
9import java.lang.reflect.InvocationTargetException;
10import java.lang.reflect.Method;
11import java.lang.reflect.Type;
12import java.net.URL;
13import java.util.ArrayList;
14import java.util.List;
15import org.xml.sax.Attributes;
16import org.xml.sax.Locator;
17import safesax.Element;
18import safesax.ElementListener;
19import safesax.Parsers;
20import safesax.RootElement;
21import safesax.StartElementListener;
22
23public class XmlBeanModule implements Module {
24
25  final URL xmlUrl;
26
27  Locator locator;
28  Binder originalBinder;
29  BeanBuilder beanBuilder;
30
31  public XmlBeanModule(URL xmlUrl) {
32    this.xmlUrl = xmlUrl;
33  }
34
35  public void configure(Binder binder) {
36    this.originalBinder = binder;
37
38    try {
39      RootElement beans = new RootElement("beans");
40      locator = beans.getLocator();
41
42      Element bean = beans.getChild("bean");
43      bean.setElementListener(new BeanListener());
44
45      Element property = bean.getChild("property");
46      property.setStartElementListener(new PropertyListener());
47
48      Reader in = new InputStreamReader(xmlUrl.openStream());
49      Parsers.parse(in, beans.getContentHandler());
50    }
51    catch (Exception e) {
52      originalBinder.addError(e);
53    }
54  }
55
56  /** Handles "binding" elements. */
57  class BeanListener implements ElementListener {
58
59    public void start(final Attributes attributes) {
60      Binder sourced = originalBinder.withSource(xmlSource());
61
62      String typeString = attributes.getValue("type");
63
64      // Make sure 'type' is present.
65      if (typeString == null) {
66        sourced.addError("Missing 'type' attribute.");
67        return;
68      }
69
70      // Resolve 'type'.
71      Class<?> type;
72      try {
73        type = Class.forName(typeString);
74      }
75      catch (ClassNotFoundException e) {
76        sourced.addError(e);
77        return;
78      }
79
80      // Look for a no-arg constructor.
81      try {
82        type.getConstructor();
83      }
84      catch (NoSuchMethodException e) {
85        sourced.addError("%s doesn't have a no-arg constructor.");
86        return;
87      }
88
89      // Create a bean builder for the given type.
90      beanBuilder = new BeanBuilder(type);
91    }
92
93    public void end() {
94      if (beanBuilder != null) {
95        beanBuilder.build();
96        beanBuilder = null;
97      }
98    }
99  }
100
101  /** Handles "property" elements. */
102  class PropertyListener implements StartElementListener {
103
104    public void start(final Attributes attributes) {
105      Binder sourced = originalBinder.withSource(xmlSource());
106
107      if (beanBuilder == null) {
108        // We must have already run into an error.
109        return;
110      }
111
112      // Check for 'name'.
113      String name = attributes.getValue("name");
114      if (name == null) {
115        sourced.addError("Missing attribute name.");
116        return;
117      }
118
119      Class<?> type = beanBuilder.type;
120
121      // Find setter method for the given property name.
122      String setterName = "set" + capitalize(name);
123      Method setter = null;
124      for (Method method : type.getMethods()) {
125        if (method.getName().equals(setterName)) {
126          setter = method;
127          break;
128        }
129      }
130      if (setter == null) {
131        sourced.addError("%s.%s() not found.", type.getName(), setterName);
132        return;
133      }
134
135      // Validate number of parameters.
136      Type[] parameterTypes = setter.getGenericParameterTypes();
137      if (parameterTypes.length != 1) {
138        sourced.addError("%s.%s() must take one argument.",
139            setterName, type.getName());
140        return;
141      }
142
143      // Add property descriptor to builder.
144      Provider<?> provider = sourced.getProvider(Key.get(parameterTypes[0]));
145      beanBuilder.properties.add(
146          new Property(setter, provider));
147    }
148  }
149
150  static String capitalize(String s) {
151    return Character.toUpperCase(s.charAt(0)) +
152        s.substring(1);
153  }
154
155  static class Property {
156
157    final Method setter;
158    final Provider<?> provider;
159
160    Property(Method setter, Provider<?> provider) {
161      this.setter = setter;
162      this.provider = provider;
163    }
164
165    void setOn(Object o) {
166      try {
167        setter.invoke(o, provider.get());
168      }
169      catch (IllegalAccessException e) {
170        throw new RuntimeException(e);
171      }
172      catch (InvocationTargetException e) {
173        throw new RuntimeException(e);
174      }
175    }
176  }
177
178  class BeanBuilder {
179
180    final List<Property> properties = new ArrayList<Property>();
181    final Class<?> type;
182
183    BeanBuilder(Class<?> type) {
184      this.type = type;
185    }
186
187    void build() {
188      addBinding(type);
189    }
190
191    <T> void addBinding(Class<T> type) {
192      originalBinder.withSource(xmlSource())
193          .bind(type).toProvider(new BeanProvider<T>(type, properties));
194    }
195  }
196
197  static class BeanProvider<T> implements Provider<T> {
198
199    final Class<T> type;
200    final List<Property> properties;
201
202    BeanProvider(Class<T> type, List<Property> properties) {
203      this.type = type;
204      this.properties = properties;
205    }
206
207    public T get() {
208      try {
209        T t = type.newInstance();
210        for (Property property : properties) {
211          property.setOn(t);
212        }
213        return t;
214      }
215      catch (InstantiationException e) {
216        throw new RuntimeException(e);
217      }
218      catch (IllegalAccessException e) {
219        throw new RuntimeException(e);
220      }
221    }
222  }
223
224  Object xmlSource() {
225    return xmlUrl + ":" + locator.getLineNumber();
226  }
227}
228