View Javadoc
1   package net.avcompris.commons3.client;
2   
3   import static com.google.common.base.Preconditions.checkArgument;
4   import static com.google.common.base.Preconditions.checkNotNull;
5   import static net.avcompris.commons3.core.DateTimeHolderImpl.toDateTimeHolder;
6   import static org.apache.commons.lang3.StringUtils.uncapitalize;
7   
8   import java.io.IOException;
9   import java.lang.annotation.Annotation;
10  import java.lang.reflect.InvocationTargetException;
11  import java.lang.reflect.Method;
12  
13  import javax.annotation.Nullable;
14  
15  import org.joda.time.DateTime;
16  
17  import com.fasterxml.jackson.core.JsonParser;
18  import com.fasterxml.jackson.core.JsonToken;
19  import com.fasterxml.jackson.core.ObjectCodec;
20  import com.fasterxml.jackson.databind.DeserializationContext;
21  import com.fasterxml.jackson.databind.JsonNode;
22  import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
23  import com.fasterxml.jackson.databind.node.NullNode;
24  
25  import net.avcompris.commons3.databeans.DataBeans;
26  import net.avcompris.commons3.types.DateTimeHolder;
27  
28  public final class DataBeanDeserializer<T> extends StdDeserializer<T> {
29  
30  	private static final long serialVersionUID = 8617288345903422915L;
31  
32  	private final Class<T> dataBeanClass;
33  
34  	public DataBeanDeserializer(final Class<T> dataBeanClass) {
35  
36  		super(checkNotNull(dataBeanClass, "dataBeanClass"));
37  
38  		checkArgument(dataBeanClass.isInterface(), //
39  				"dataBeanClass should be an interface, but was: %s", dataBeanClass.getName());
40  
41  		this.dataBeanClass = dataBeanClass;
42  	}
43  
44  	public static <U> DataBeanDeserializer<U> createDeserializer(final Class<U> clazz) {
45  
46  		return new DataBeanDeserializer<U>(clazz);
47  	}
48  
49  	private static boolean hasParameterAnnotation(final Method method, final int paramIndex,
50  			final Class<? extends Annotation> annotationClass) {
51  
52  		// Circumvent the "default" method mechanism: Find the outermost corresponding
53  		// method
54  		//
55  		if (method.isDefault()) {
56  
57  			final Method outermostMethod;
58  
59  			try {
60  
61  				outermostMethod = method.getDeclaringClass().getMethod(method.getName(), method.getParameterTypes());
62  
63  			} catch (final NoSuchMethodException e) {
64  
65  				throw new RuntimeException(e);
66  			}
67  
68  			if (!outermostMethod.isDefault()) {
69  
70  				return hasParameterAnnotation(outermostMethod, paramIndex, annotationClass);
71  			}
72  		}
73  
74  		for (final Annotation annotation : method.getParameterAnnotations()[paramIndex]) {
75  
76  			if (annotationClass.isInstance(annotation)) {
77  
78  				return true;
79  			}
80  		}
81  
82  		return false;
83  	}
84  
85  	@Override
86  	public T deserialize(final JsonParser parser, final DeserializationContext context) throws IOException {
87  
88  		checkNotNull(parser, "parser");
89  		checkNotNull(context, "context");
90  
91  		final T dataBean = DataBeans.instantiate(dataBeanClass);
92  
93  		final ObjectCodec codec = parser.getCodec();
94  
95  		final JsonNode node = codec.readTree(parser);
96  
97  		for (final Method method : dataBeanClass.getMethods()) {
98  
99  			final String methodName = method.getName();
100 
101 			if (!methodName.startsWith("set")) {
102 				continue;
103 			}
104 
105 			final Class<?>[] paramTypes = method.getParameterTypes();
106 
107 			if (paramTypes == null || paramTypes.length != 1) {
108 				continue;
109 			}
110 
111 			final Class<?> paramType = paramTypes[0];
112 
113 			final boolean isNullable;
114 
115 			if (hasParameterAnnotation(method, 0, Nullable.class)) {
116 
117 				isNullable = true;
118 
119 			} else {
120 
121 				isNullable = false;
122 			}
123 
124 			final String fieldName = uncapitalize(methodName.substring(3)); // Skip "set"
125 
126 			final JsonNode fieldNode = node.get(fieldName);
127 
128 			if (fieldNode == null || fieldNode instanceof NullNode) {
129 
130 				if (isNullable) {
131 					continue;
132 				}
133 
134 				throw new IOException("Non-@Nullable field is missing for DataBean: " + dataBeanClass.getName() + "."
135 						+ fieldName + ": " + fieldNode);
136 			}
137 
138 			final Object value;
139 
140 			if (Boolean.class.equals(paramType) || boolean.class.equals(paramType)) {
141 
142 				try {
143 
144 					value = Boolean.parseBoolean(fieldNode.asText());
145 
146 				} catch (final NumberFormatException e) {
147 					throw new IOException("Cannot parse boolean field for DataBean: " + dataBeanClass.getName() + "."
148 							+ fieldName + ": " + fieldNode);
149 				}
150 
151 			} else if (Integer.class.equals(paramType) || int.class.equals(paramType)) {
152 
153 				try {
154 
155 					value = Integer.parseInt(fieldNode.asText());
156 
157 				} catch (final NumberFormatException e) {
158 					throw new IOException("Cannot parse int field for DataBean: " + dataBeanClass.getName() + "."
159 							+ fieldName + ": " + fieldNode);
160 				}
161 
162 			} else if (Short.class.equals(paramType) || short.class.equals(paramType)) {
163 
164 				try {
165 
166 					value = Short.parseShort(fieldNode.asText());
167 
168 				} catch (final NumberFormatException e) {
169 					throw new IOException("Cannot parse short field for DataBean: " + dataBeanClass.getName() + "."
170 							+ fieldName + ": " + fieldNode);
171 				}
172 
173 			} else if (Long.class.equals(paramType) || long.class.equals(paramType)) {
174 
175 				try {
176 
177 					value = Long.parseLong(fieldNode.asText());
178 
179 				} catch (final NumberFormatException e) {
180 					throw new IOException("Cannot parse long field for DataBean: " + dataBeanClass.getName() + "."
181 							+ fieldName + ": " + fieldNode);
182 				}
183 
184 			} else if (String.class.equals(paramType)) {
185 
186 				value = fieldNode.asText();
187 
188 			} else if (DateTimeHolder.class.equals(paramType)) {
189 
190 				final DateTime dateTime;
191 
192 				try {
193 
194 					dateTime = DateTime.parse(fieldNode.get("dateTime").asText());
195 
196 				} catch (final IllegalArgumentException e) {
197 					throw new IOException("Cannot parse DateTime field for DataBean: " + dataBeanClass.getName() + "."
198 							+ fieldName + ": " + fieldNode);
199 				}
200 
201 				// final long timestamp = fieldNode.has("timestamp") //
202 				// ? fieldNode.get("timestamp").asLong() //
203 				// : dateTime.getMillis();
204 
205 				value = toDateTimeHolder(dateTime);
206 
207 			} else {
208 
209 				final JsonParser subParser = fieldNode.traverse();
210 
211 				final JsonToken valueToken = subParser.nextToken();
212 
213 				if (valueToken == JsonToken.VALUE_NULL) {
214 
215 					if (isNullable) {
216 
217 						value = null;
218 
219 					} else {
220 
221 						throw new IOException("Non-@Nullable field is missing for DataBean: " + dataBeanClass.getName()
222 								+ "." + fieldName + ": " + valueToken);
223 					}
224 
225 				} else {
226 
227 					subParser.setCodec(codec);
228 
229 					value = context.readValue(subParser, paramType);
230 				}
231 			}
232 
233 			method.setAccessible(true);
234 
235 			try {
236 
237 				method.invoke(dataBean, value);
238 
239 			} catch (final InvocationTargetException e) {
240 
241 				e.printStackTrace();
242 
243 				final Throwable targetException = e.getTargetException();
244 
245 				if (targetException == null) {
246 					throw new RuntimeException(e);
247 				} else if (targetException instanceof Error) {
248 					throw (Error) targetException;
249 				} else if (targetException instanceof RuntimeException) {
250 					throw (RuntimeException) targetException;
251 				} else {
252 					throw new RuntimeException(targetException);
253 				}
254 
255 			} catch (final IllegalAccessException e) {
256 
257 				e.printStackTrace();
258 
259 				throw new RuntimeException(e);
260 			}
261 		}
262 
263 		return dataBean;
264 	}
265 }