In preparation for submitting a patch with Java8 Streams to GWT, I've been 
using streams in more situations, and have found a case with AutoBeans 
where they don't behave correctly. Specifically, AutoBeanFactoryGenerator 
builds a "shim" type for each object that it will interact with, starting 
with the bean types declared on the factory, and recursively handling each 
type it encounters as return types from declared methods.

ABFGenerator doesn't have any special wiring for List/Map/Set, which 
surprised me, since there is a specific class to wrap Splittable instances 
rather than getting an autobean wrapper/shim type - 
the SplittableList, SplittableSet and SplittableComplex/SimpleMap types 
serve this purpose - at least I thought it did. Upon reading more deeply, 
it seems that the generated type emul.java.util.ListAutoBean is used if 

   1. a wrapped (not-splittable) autobean
   2. has a method that returns List, and
   3. the returned value from that method/getter is not itself an autobean.
   

In that case, the 'normal' list type gets wrapped, and the shim delegates 
all of its calls to the non-autobean list. Not what I had thought happened, 
but hey, great to learn. I had assumed that the generics issue would get in 
the way of this - no autobean type can actually be generic, since when we 
turn the JS into actual Java types, we can't just cast at will - the 
interfaces are not backed by JSOs after all (until you look at Splittable, 
but that's a different story). The generator will give you nice errors if 
you attempt to register a generic type *other* than by wrapping an existing 
type.

----

The first problem I've run into is in adding a method like Java8's 
Collection.stream() or Collection.spliterator(). For Spliterator, for 
example, it attempts to generate a type emul.java.util.SpliteratorAutoBean, 
which means the method getComparator():
public class SpliteratorAutoBean extends com.google.web.bindery.autobean.
shared.impl.AbstractAutoBean<java.util.Spliterator> {
  private final java.util.Spliterator shim = new java.util.Spliterator() {
    public java.util.Comparator getComparator()  {
      java.util.Comparator toReturn = SpliteratorAutoBean.this.getWrapped().
getComparator();
      if (toReturn != null) {
        if (SpliteratorAutoBean.this.isWrapped(toReturn)) {
          toReturn = SpliteratorAutoBean.this.getFromWrapper(toReturn);
        } else {
          toReturn = new emul.java.util.ComparatorAutoBean(getFactory(), 
toReturn).as();
        }
      }
      return toReturn;
    }


So then a ComparatorAutoBean is generated, and in turn, it must have all of 
its method emulated. Unfortunately, it gets a few of these wrong 
(ABFGenerator doesn't think to avoid wrapping static methods):
    public java.util.Comparator naturalOrder()  {
      java.util.Comparator toReturn = ComparatorAutoBean.this.getWrapped().
naturalOrder();
      if (toReturn != null) {
        if (ComparatorAutoBean.this.isWrapped(toReturn)) {
          toReturn = ComparatorAutoBean.this.getFromWrapper(toReturn);
        } else {
          toReturn = new emul.java.util.ComparatorAutoBean(getFactory(), 
toReturn).as();
        }
      }
      ComparatorAutoBean.this.call("naturalOrder", toReturn );
      return toReturn;
    }

This is solved easily enough by checking to see if a method is static as we 
iterate through and build up shims. However, this isn't sufficient to make 
the SpliteratorAutoBean sane. 

----

Quick reminder: methods in autobeans are in four varieties - GET, SET, 
SET_BUILDER, and CALL. SET_BUILDER is a SET that returns 'this' so you can 
have a nice builder api, and anything that doesn't look like a getter or a 
setter is made into a CALL. Here's a funny thing - 
Splitterator.getComparator() looks like a GET, not a CALL. 
Spliterator.getExactSizeIfKnown() also is a GET, but on the other hand, 
Spliterator.hasCharacteristics(int) is a CALL, since it has a parameter. 
This means that we a) descend and generate a ComparatorAutoBean type as 
above, but also that the AutoBeanVisitor wiring must be able to also 
descend into this. This introduces our second problem (in bold):
  @Override protected void traverseProperties(com.google.web.bindery.
autobean.shared.AutoBeanVisitor visitor, com.google.web.bindery.autobean.
shared.impl.AbstractAutoBean.OneShotContext ctx) {
    com.google.web.bindery.autobean.shared.impl.AbstractAutoBean bean;
    Object value;
    com.google.web.bindery.autobean.gwt.client.impl.ClientPropertyContext 
propertyContext;
    java.util.Spliterator as = as();
    bean = (com.google.web.bindery.autobean.shared.impl.AbstractAutoBean) 
com.google.web.bindery.autobean.shared.AutoBeanUtils.getAutoBean(as.
getComparator());
    propertyContext = new com.google.web.bindery.autobean.gwt.client.impl.
ClientPropertyContext(
      as,
      com.google.web.bindery.autobean.gwt.client.impl.ClientPropertyContext.
Setter.beanSetter(SpliteratorAutoBean.this, "comparator"),
      new Class<?>[] {java.util.Comparator.class, *T **extends 
java.lang.Object.class*},
      new int[] {1, 0}
    );
    if (visitor.visitReferenceProperty("comparator", bean, propertyContext)) 
{
      if (bean != null) { bean.traverse(visitor, ctx); }
    }
    visitor.endVisitReferenceProperty("comparator", bean, propertyContext);
    value = as.getExactSizeIfKnown();
    propertyContext = new com.google.web.bindery.autobean.gwt.client.impl.
ClientPropertyContext(
      as,
      com.google.web.bindery.autobean.gwt.client.impl.ClientPropertyContext.
Setter.beanSetter(SpliteratorAutoBean.this, "exactSizeIfKnown"),
      long.class
    );
    if (visitor.visitValueProperty("exactSizeIfKnown", value, 
propertyContext)) {
    }
    visitor.endVisitValueProperty("exactSizeIfKnown", value, propertyContext
);
  }



This gets us back to the generics problem - our SpliteratorAutoBean gives 
up the Spliterator<T> generic T when it implements it, but that T still 
matters for the return type of Spliterator.getComparator(). This doesn't 
bite us for Collection's spliterator() or stream() methods since they are 
CALLs, but GETs do require that they behave, and Java8 didn't check to see 
if AutoBeans would get along with them before they added these new methods.


So here's why I have this as a long email, instead of simply as a submitted 
patch, landed before https://gwt-review.googlesource.com/#/c/13730/ and the 
streams patch - how do we resolve this:


   - Do we work harder to clean up the ClientPropertyContext type data and 
   just toss back Object.class? (my preferred solution, seems cleanest)
   - Or do we go deeper, since this isn't actually a property at all, and 
   somehow blacklist these GET AutoBeanMethods from being actually marked as 
   GET, so that we skip this issue? (unsure how to go about this in a sane 
   way, but very open to suggestions, since this gets worse in our third issue)
   

For the first option, ModelUtils.ensureBaseType() is the culprit - we are 
calling ensureBaseType(JWildCardType[class ? super T]). 

  @SuppressWarnings("unchecked")
  public static <T extends JType> T ensureBaseType(T maybeParameterized) {
    if (maybeParameterized.isArray() != null) {
      JArrayType array = maybeParameterized.isArray();
      return (T) array.getOracle().getArrayType(
          ensureBaseType(array.getComponentType()));
    }
    if (maybeParameterized.isTypeParameter() != null) {
      return (T) maybeParameterized.isTypeParameter().getBaseType();
    }
    if (maybeParameterized.isParameterized() != null) {
      return (T) maybeParameterized.isParameterized().getBaseType();
    }
    if (maybeParameterized.isRawType() != null) {
      return (T) maybeParameterized.isRawType().getBaseType();
    }
    if (maybeParameterized.isWildcard() != null) {
      return (T) maybeParameterized.isWildcard().getBaseType();
    }
    return maybeParameterized;
  }


We correctly identify this as a wildcard type, and attempt to return the 
base type, which is not enough - it is a JTypeParameter[class T extends 
java.lang.Object]. The base type of *that* then is our goal, a 
JRealClassType[java.lang.Object]. As such, calling ensureBaseType() on 
getBaseType() from *at least* the return value from isWildcard will fix 
this, though I'd like someone who knows more about these types to confirm 
how nested and ugly types can get.


----


Finally, a third issue, where our raw types bites us harder:
public class StreamAutoBean extends com.google.web.bindery.autobean.shared.
impl.AbstractAutoBean<*java.util.stream.Stream*> {
  private final java.util.stream.Stream shim = new *java*
*.util.stream.Stream*() {

...

    public java.util.stream.Stream onClose(java.lang.Runnable closeHandler) 
 {
      *java**.util.stream.Stream* toReturn = StreamAutoBean.this.getWrapped
().*onClose*(closeHandler);
      if (toReturn != null) {
        if (StreamAutoBean.this.isWrapped(toReturn)) {
          toReturn = StreamAutoBean.this.getFromWrapper(toReturn);
        } else {
          toReturn = new emul.java.util.stream.StreamAutoBean(getFactory(), 
toReturn).as();
        }
      }
      StreamAutoBean.this.call("onClose", toReturn, closeHandler);
      return toReturn;
    }


(For convenience, getWrapped in the superclass AbstractAutoBean:
public abstract class AbstractAutoBean<T> implements AutoBean<T>, 
HasSplittable {

...

  protected T getWrapped() {
    checkWrapped();
    return wrapped;
  }

)


The compiler error (occurs both in JavaC as well as GWT, so its not GWT 
specific):
[ERROR] Line 290: Type mismatch: cannot convert from BaseStream to Stream

or
Error:(290, 88) java: incompatible types: java.util.stream.BaseStream 
cannot be converted to java.util.stream.Stream


A little more context (from the stream patch, based entirely on types 
declared in Javadocs):

public interface Stream<T> extends BaseStream<T, *Stream<T**>*>


public interface BaseStream<T,S extends BaseStream<T,*S*>> {

...

  *S* onClose(Runnable closeHandler);


The problem is that since our AbstractAutoBean is only generic on Stream, 
Java apparently throws away other type details. Since we don't specify the 
T for Stream, we don't get to say what S will be in BaseStream<T,S>, and so 
onClose can only return the raw type, BaseStream. Fixing this is either 
introducing generics correctly all over all generated autobeans, or casting 
the crap out of values that come out of methods that should be generic. 
Casting seems to be the expedient solution for every toReturn value, but 
its also clearly not a very nice one, and not necessary anywhere except for 
cases where generics are allowed. And, as mentioned, autobeans really don't 
play nicely with generics anyway, so perhaps blacklisting should be back on 
the table.


With the cast-all-the-things approach, this final case compiles too. 


----


Proposed patch at https://gwt-review.googlesource.com/14410.

-- 
You received this message because you are subscribed to the Google Groups "GWT 
Contributors" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to google-web-toolkit-contributors+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/google-web-toolkit-contributors/78588fa5-c831-4b32-8df4-57b1525ff86c%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to