1 package example.xml;
2 
3 import com.google.inject.Key;
4 import com.google.inject.Module;
5 import com.google.inject.Provider;
6 import com.google.inject.Binder;
7 import java.io.InputStreamReader;
8 import java.io.Reader;
9 import java.lang.reflect.InvocationTargetException;
10 import java.lang.reflect.Method;
11 import java.lang.reflect.Type;
12 import java.net.URL;
13 import java.util.ArrayList;
14 import java.util.List;
15 import org.xml.sax.Attributes;
16 import org.xml.sax.Locator;
17 import safesax.Element;
18 import safesax.ElementListener;
19 import safesax.Parsers;
20 import safesax.RootElement;
21 import safesax.StartElementListener;
22 
23 public class XmlBeanModule implements Module {
24 
25   final URL xmlUrl;
26 
27   Locator locator;
28   Binder originalBinder;
29   BeanBuilder beanBuilder;
30 
XmlBeanModule(URL xmlUrl)31   public XmlBeanModule(URL xmlUrl) {
32     this.xmlUrl = xmlUrl;
33   }
34 
configure(Binder binder)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 
start(final Attributes attributes)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 
end()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 
start(final Attributes attributes)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 
capitalize(String s)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 
Property(Method setter, Provider<?> provider)160     Property(Method setter, Provider<?> provider) {
161       this.setter = setter;
162       this.provider = provider;
163     }
164 
setOn(Object o)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 
BeanBuilder(Class<?> type)183     BeanBuilder(Class<?> type) {
184       this.type = type;
185     }
186 
build()187     void build() {
188       addBinding(type);
189     }
190 
addBinding(Class<T> type)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 
BeanProvider(Class<T> type, List<Property> properties)202     BeanProvider(Class<T> type, List<Property> properties) {
203       this.type = type;
204       this.properties = properties;
205     }
206 
get()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 
xmlSource()224   Object xmlSource() {
225     return xmlUrl + ":" + locator.getLineNumber();
226   }
227 }
228