blob: 78b4f2e4eed6fa6363e9584eb9d76bc54606f11e [file] [log] [blame]
bobv@google.coma29cb152011-04-08 14:17:10 +00001/*
2 * Copyright 2011 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
bobv@google.comb0068d72011-04-18 09:42:06 +000016package com.google.web.bindery.autobean.shared.impl;
bobv@google.coma29cb152011-04-08 14:17:10 +000017
bobv@google.comb0068d72011-04-18 09:42:06 +000018import com.google.web.bindery.autobean.shared.AutoBean;
19import com.google.web.bindery.autobean.shared.AutoBeanFactory;
20import com.google.web.bindery.autobean.shared.AutoBeanUtils;
21import com.google.web.bindery.autobean.shared.AutoBeanVisitor;
22import com.google.web.bindery.autobean.shared.AutoBeanVisitor.ParameterizationVisitor;
23import com.google.web.bindery.autobean.shared.Splittable;
24import com.google.web.bindery.autobean.shared.ValueCodex;
bobv@google.coma29cb152011-04-08 14:17:10 +000025
26import java.util.ArrayList;
27import java.util.Collection;
28import java.util.HashMap;
29import java.util.Iterator;
30import java.util.List;
31import java.util.Map;
32import java.util.Set;
33import java.util.Stack;
34
35/**
36 * Contains the implementation details of AutoBeanCodex. This type was factored
37 * out of AutoBeanCodex so that various implementation details can be accessed
38 * without polluting a public API.
39 */
40public class AutoBeanCodexImpl {
41
42 /**
43 * Describes a means of encoding or decoding a particular type of data to or
44 * from a wire format representation. Any given instance of a Coder should be
45 * stateless; any state required for operation must be maintained in an
46 * {@link EncodeState}.
47 */
48 public interface Coder {
49 Object decode(EncodeState state, Splittable data);
50
51 void encode(EncodeState state, Object value);
52
53 Splittable extractSplittable(EncodeState state, Object value);
54 }
55
56 /**
57 * Contains transient state for Coder operation.
58 */
59 public static class EncodeState {
60 /**
61 * Constructs a state object used for decoding payloads.
62 */
63 public static EncodeState forDecode(AutoBeanFactory factory) {
64 return new EncodeState(factory, null);
65 }
66
67 /**
68 * Constructs a state object used for encoding payloads.
69 */
70 public static EncodeState forEncode(AutoBeanFactory factory, StringBuilder sb) {
71 return new EncodeState(factory, sb);
72 }
73
74 /**
75 * Constructs a "stateless" state for testing Coders that do not require
76 * AutoBean implementation details.
77 */
78 public static EncodeState forTesting() {
79 return new EncodeState(null, null);
80 }
81
82 final EnumMap enumMap;
83 final AutoBeanFactory factory;
84 final StringBuilder sb;
85 final Stack<AutoBean<?>> seen;
86
87 private EncodeState(AutoBeanFactory factory, StringBuilder sb) {
88 this.factory = factory;
89 enumMap = factory instanceof EnumMap ? (EnumMap) factory : null;
90 this.sb = sb;
91 this.seen = sb == null ? null : new Stack<AutoBean<?>>();
92 }
93 }
94
95 /**
96 * Dynamically creates a Coder that is capable of operating on a particular
97 * parameterization of a datastructure (e.g. {@code Map<String, List<String>>}
98 * ).
99 */
100 static class CoderCreator extends ParameterizationVisitor {
101 private Stack<Coder> stack = new Stack<Coder>();
102
103 @Override
104 public void endVisitType(Class<?> type) {
105 if (List.class.equals(type) || Set.class.equals(type)) {
106 stack.push(collectionCoder(type, stack.pop()));
107 } else if (Map.class.equals(type)) {
108 // Note that the parameters are passed in reverse order
109 stack.push(mapCoder(stack.pop(), stack.pop()));
110 } else if (Splittable.class.equals(type)) {
111 stack.push(splittableCoder());
112 } else if (type.getEnumConstants() != null) {
113 @SuppressWarnings(value = {"unchecked"})
114 Class<Enum<?>> enumType = (Class<Enum<?>>) type;
115 stack.push(enumCoder(enumType));
116 } else if (ValueCodex.canDecode(type)) {
117 stack.push(valueCoder(type));
118 } else {
119 stack.push(objectCoder(type));
120 }
121 }
122
123 public Coder getCoder() {
124 assert stack.size() == 1 : "Incorrect size: " + stack.size();
125 return stack.pop();
126 }
127 }
128
129 /**
130 * Constructs one of the lightweight collection types.
131 */
132 static class CollectionCoder implements Coder {
133 private final Coder elementDecoder;
134 private final Class<?> type;
135
136 public CollectionCoder(Class<?> type, Coder elementDecoder) {
137 this.elementDecoder = elementDecoder;
138 this.type = type;
139 }
140
141 public Object decode(EncodeState state, Splittable data) {
142 Collection<Object> collection;
143 if (List.class.equals(type)) {
144 collection = new SplittableList<Object>(data, elementDecoder, state);
145 } else if (Set.class.equals(type)) {
146 collection = new SplittableSet<Object>(data, elementDecoder, state);
147 } else {
148 // Should not reach here
149 throw new RuntimeException(type.getName());
150 }
151 return collection;
152 }
153
154 public void encode(EncodeState state, Object value) {
155 if (value == null) {
156 state.sb.append("null");
157 return;
158 }
159
160 Iterator<?> it = ((Collection<?>) value).iterator();
161 state.sb.append("[");
162 if (it.hasNext()) {
163 elementDecoder.encode(state, it.next());
164 while (it.hasNext()) {
165 state.sb.append(",");
166 elementDecoder.encode(state, it.next());
167 }
168 }
169 state.sb.append("]");
170 }
171
172 public Splittable extractSplittable(EncodeState state, Object value) {
173 return tryExtractSplittable(value);
174 }
175 }
176
177 /**
178 * Produces enums.
179 *
180 * @param <E>
181 */
182 static class EnumCoder<E extends Enum<?>> implements Coder {
183 private final Class<E> type;
184
185 public EnumCoder(Class<E> type) {
186 this.type = type;
187 }
188
189 public Object decode(EncodeState state, Splittable data) {
190 return state.enumMap.getEnum(type, data.asString());
191 }
192
193 public void encode(EncodeState state, Object value) {
194 if (value == null) {
195 state.sb.append("null");
196 return;
197 }
198 state.sb.append(StringQuoter.quote(state.enumMap.getToken((Enum<?>) value)));
199 }
200
201 public Splittable extractSplittable(EncodeState state, Object value) {
202 return StringQuoter.split(StringQuoter.quote(state.enumMap.getToken((Enum<?>) value)));
203 }
204 }
205
206 /**
207 * Used to stop processing.
208 */
209 static class HaltException extends RuntimeException {
210 public HaltException(RuntimeException cause) {
211 super(cause);
212 }
213
214 @Override
215 public RuntimeException getCause() {
216 return (RuntimeException) super.getCause();
217 }
218 }
219
220 /**
221 * Constructs one of the lightweight Map types, depending on the key type.
222 */
223 static class MapCoder implements Coder {
224 private final Coder keyDecoder;
225 private final Coder valueDecoder;
226
227 /**
228 * Parameters in reversed order to accommodate stack-based setup.
229 */
230 public MapCoder(Coder valueDecoder, Coder keyDecoder) {
231 this.keyDecoder = keyDecoder;
232 this.valueDecoder = valueDecoder;
233 }
234
235 public Object decode(EncodeState state, Splittable data) {
236 Map<Object, Object> toReturn;
237 if (data.isIndexed()) {
238 assert data.size() == 2 : "Wrong data size: " + data.size();
239 toReturn = new SplittableComplexMap<Object, Object>(data, keyDecoder, valueDecoder, state);
240 } else {
241 toReturn = new SplittableSimpleMap<Object, Object>(data, keyDecoder, valueDecoder, state);
242 }
243 return toReturn;
244 }
245
246 public void encode(EncodeState state, Object value) {
247 if (value == null) {
248 state.sb.append("null");
249 return;
250 }
251
252 Map<?, ?> map = (Map<?, ?>) value;
253 boolean isSimpleMap = keyDecoder instanceof ValueCoder;
254 if (isSimpleMap) {
255 boolean first = true;
256 state.sb.append("{");
257 for (Map.Entry<?, ?> entry : map.entrySet()) {
258 Object mapKey = entry.getKey();
259 if (mapKey == null) {
260 // A null key in a simple map is meaningless
261 continue;
262 }
263 Object mapValue = entry.getValue();
264
265 if (first) {
266 first = false;
267 } else {
268 state.sb.append(",");
269 }
270
271 keyDecoder.encode(state, mapKey);
272 state.sb.append(":");
273 if (mapValue == null) {
274 // Null values must be preserved
275 state.sb.append("null");
276 } else {
277 valueDecoder.encode(state, mapValue);
278 }
279 }
280 state.sb.append("}");
281 } else {
282 List<Object> keys = new ArrayList<Object>(map.size());
283 List<Object> values = new ArrayList<Object>(map.size());
284 for (Map.Entry<?, ?> entry : map.entrySet()) {
285 keys.add(entry.getKey());
286 values.add(entry.getValue());
287 }
288 state.sb.append("[");
289 collectionCoder(List.class, keyDecoder).encode(state, keys);
290 state.sb.append(",");
291 collectionCoder(List.class, valueDecoder).encode(state, values);
292 state.sb.append("]");
293 }
294 }
295
296 public Splittable extractSplittable(EncodeState state, Object value) {
297 return tryExtractSplittable(value);
298 }
299 }
300
301 /**
302 * Recurses into {@link AutoBeanCodexImpl}.
303 */
304 static class ObjectCoder implements Coder {
305 private final Class<?> type;
306
307 public ObjectCoder(Class<?> type) {
308 this.type = type;
309 }
310
311 public Object decode(EncodeState state, Splittable data) {
312 AutoBean<?> bean = doDecode(state, type, data);
313 return bean == null ? null : bean.as();
314 }
315
316 public void encode(EncodeState state, Object value) {
317 if (value == null) {
318 state.sb.append("null");
319 return;
320 }
321 doEncode(state, AutoBeanUtils.getAutoBean(value));
322 }
323
324 public Splittable extractSplittable(EncodeState state, Object value) {
325 return tryExtractSplittable(value);
326 }
327 }
328
329 static class PropertyCoderCreator extends AutoBeanVisitor {
330 private AutoBean<?> bean;
331
332 @Override
333 public boolean visit(AutoBean<?> bean, Context ctx) {
334 this.bean = bean;
335 return true;
336 }
337
338 @Override
339 public boolean visitReferenceProperty(String propertyName, AutoBean<?> value,
340 PropertyContext ctx) {
341 maybeCreateCoder(propertyName, ctx);
342 return false;
343 }
344
345 @Override
346 public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) {
347 maybeCreateCoder(propertyName, ctx);
348 return false;
349 }
350
351 private void maybeCreateCoder(String propertyName, PropertyContext ctx) {
352 CoderCreator creator = new CoderCreator();
353 ctx.accept(creator);
354 coderFor.put(key(bean, propertyName), creator.getCoder());
355 }
356 }
357
358 /**
359 * Extracts properties from a bean and turns them into JSON text.
360 */
361 static class PropertyGetter extends AutoBeanVisitor {
362 private boolean first = true;
363 private final EncodeState state;
364
365 public PropertyGetter(EncodeState state) {
366 this.state = state;
367 }
368
369 @Override
370 public void endVisit(AutoBean<?> bean, Context ctx) {
371 state.sb.append("}");
372 state.seen.pop();
373 }
374
375 @Override
376 public boolean visit(AutoBean<?> bean, Context ctx) {
377 if (state.seen.contains(bean)) {
378 throw new HaltException(new UnsupportedOperationException("Cycles not supported"));
379 }
380 state.seen.push(bean);
381 state.sb.append("{");
382 return true;
383 }
384
385 @Override
386 public boolean visitReferenceProperty(String propertyName, AutoBean<?> value,
387 PropertyContext ctx) {
388 if (value != null) {
389 encodeProperty(propertyName, value.as(), ctx);
390 }
391 return false;
392 }
393
394 @Override
395 public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) {
396 if (value != null && !value.equals(ValueCodex.getUninitializedFieldValue(ctx.getType()))) {
397 encodeProperty(propertyName, value, ctx);
398 }
399 return false;
400 }
401
402 private void encodeProperty(String propertyName, Object value, PropertyContext ctx) {
403 CoderCreator pd = new CoderCreator();
404 ctx.accept(pd);
405 Coder decoder = pd.getCoder();
406 if (first) {
407 first = false;
408 } else {
409 state.sb.append(",");
410 }
411 state.sb.append(StringQuoter.quote(propertyName));
412 state.sb.append(":");
413 decoder.encode(state, value);
414 }
415 }
416
417 /**
418 * Populates beans with data extracted from an evaluated JSON payload.
419 */
420 static class PropertySetter extends AutoBeanVisitor {
421 private Splittable data;
422 private EncodeState state;
423
424 public void decodeInto(EncodeState state, Splittable data, AutoBean<?> bean) {
425 this.data = data;
426 this.state = state;
427 bean.accept(this);
428 }
429
430 @Override
431 public boolean visitReferenceProperty(String propertyName, AutoBean<?> value,
432 PropertyContext ctx) {
433 decodeProperty(propertyName, ctx);
434 return false;
435 }
436
437 @Override
438 public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) {
439 decodeProperty(propertyName, ctx);
440 return false;
441 }
442
443 protected void decodeProperty(String propertyName, PropertyContext ctx) {
444 if (!data.isNull(propertyName)) {
445 CoderCreator pd = new CoderCreator();
446 ctx.accept(pd);
447 Coder decoder = pd.getCoder();
448 Object propertyValue = decoder.decode(state, data.get(propertyName));
449 ctx.set(propertyValue);
450 }
451 }
452 }
453
454 /**
455 * A passthrough Coder.
456 */
457 static class SplittableCoder implements Coder {
458 static final Coder INSTANCE = new SplittableCoder();
459
460 public Object decode(EncodeState state, Splittable data) {
461 return data;
462 }
463
464 public void encode(EncodeState state, Object value) {
465 if (value == null) {
466 state.sb.append("null");
467 return;
468 }
469 state.sb.append(((Splittable) value).getPayload());
470 }
471
472 public Splittable extractSplittable(EncodeState state, Object value) {
473 return (Splittable) value;
474 }
475 }
476
477 /**
478 * Delegates to ValueCodex.
479 */
480 static class ValueCoder implements Coder {
481 private final Class<?> type;
482
483 public ValueCoder(Class<?> type) {
484 assert type.getEnumConstants() == null : "Should use EnumTypeCodex";
485 this.type = type;
486 }
487
488 public Object decode(EncodeState state, Splittable propertyValue) {
489 if (propertyValue == null || propertyValue == Splittable.NULL) {
490 return ValueCodex.getUninitializedFieldValue(type);
491 }
492 return ValueCodex.decode(type, propertyValue);
493 }
494
495 public void encode(EncodeState state, Object value) {
496 state.sb.append(ValueCodex.encode(type, value).getPayload());
497 }
498
499 public Splittable extractSplittable(EncodeState state, Object value) {
500 return ValueCodex.encode(type, value);
501 }
502 }
503
504 /**
505 * A map of AutoBean interface+property names to the Coder for that property.
506 */
507 private static final Map<String, Coder> coderFor = new HashMap<String, Coder>();
508 /**
509 * A map of types to a Coder that handles the type.
510 */
511 private static final Map<Class<?>, Coder> coders = new HashMap<Class<?>, Coder>();
512
513 public static Coder collectionCoder(Class<?> type, Coder elementCoder) {
514 return new CollectionCoder(type, elementCoder);
515 }
516
517 public static Coder doCoderFor(AutoBean<?> bean, String propertyName) {
518 String key = key(bean, propertyName);
519 Coder toReturn = coderFor.get(key);
520 if (toReturn == null) {
521 bean.accept(new PropertyCoderCreator());
522 toReturn = coderFor.get(key);
523 if (toReturn == null) {
524 throw new IllegalArgumentException(propertyName);
525 }
526 }
527 return toReturn;
528 }
529
530 public static <T> AutoBean<T> doDecode(EncodeState state, Class<T> clazz, Splittable data) {
531 /*
532 * If we decode the same Splittable twice, re-use the ProxyAutoBean to
533 * maintain referential integrity. If we didn't do this, either facade would
534 * update the same backing data, yet not be the same object via ==
535 * comparison.
536 */
537 @SuppressWarnings("unchecked")
538 AutoBean<T> toReturn = (AutoBean<T>) data.getReified(AutoBeanCodexImpl.class.getName());
539 if (toReturn != null) {
540 return toReturn;
541 }
542 toReturn = state.factory.create(clazz);
543 data.setReified(AutoBeanCodexImpl.class.getName(), toReturn);
544 if (toReturn == null) {
545 throw new IllegalArgumentException(clazz.getName());
546 }
547 ((AbstractAutoBean<T>) toReturn).setData(data);
548 return toReturn;
549 }
550
551 public static void doDecodeInto(EncodeState state, Splittable data, AutoBean<?> bean) {
552 new PropertySetter().decodeInto(state, data, bean);
553 }
554
555 public static void doEncode(EncodeState state, AutoBean<?> bean) {
556 PropertyGetter e = new PropertyGetter(state);
557 try {
558 bean.accept(e);
559 } catch (HaltException ex) {
560 throw ex.getCause();
561 }
562 }
563
564 public static <E extends Enum<?>> Coder enumCoder(Class<E> type) {
565 Coder toReturn = coders.get(type);
566 if (toReturn == null) {
567 toReturn = new EnumCoder<E>(type);
568 coders.put(type, toReturn);
569 }
570 return toReturn;
571 }
572
573 public static Coder mapCoder(Coder valueCoder, Coder keyCoder) {
574 return new MapCoder(valueCoder, keyCoder);
575 }
576
577 public static Coder objectCoder(Class<?> type) {
578 Coder toReturn = coders.get(type);
579 if (toReturn == null) {
580 toReturn = new ObjectCoder(type);
581 coders.put(type, toReturn);
582 }
583 return toReturn;
584 }
585
586 public static Coder splittableCoder() {
587 return SplittableCoder.INSTANCE;
588 }
589
590 public static Coder valueCoder(Class<?> type) {
591 Coder toReturn = coders.get(type);
592 if (toReturn == null) {
593 toReturn = new ValueCoder(type);
594 coders.put(type, toReturn);
595 }
596 return toReturn;
597 }
598
599 static Splittable tryExtractSplittable(Object value) {
600 AutoBean<?> bean = AutoBeanUtils.getAutoBean(value);
601 if (bean != null) {
602 value = bean;
603 }
604 if (bean instanceof HasSplittable) {
605 return ((HasSplittable) bean).getSplittable();
606 }
607 return null;
608 }
609
610 private static String key(AutoBean<?> bean, String propertyName) {
611 return bean.getType().getName() + ":" + propertyName;
612 }
613}