How to cast an object to JsType?

1.2k views Asked by At

I've declare the following JsType in order to manipulate [GeoJson][1] compliant data :

@JsType
public class FeatureCollection extends GeoJson {

    @JsProperty
    private Feature[] features;

    public FeatureCollection() {
        super("FeatureCollection");
        features = new Feature[]{};
    }

    public Feature[] getFeatures() {
        return features;
    }

Sometimes I need to pass my FeatureCollection object to external libraries (like Turfs.js for instance to perform unit conversion) which access data throught features properties. The lib returns me a new object with same properties (they follow the GeoJson RFC like my JsType) but I can't cast it back to FeatureCollection:

FeatureCollection fc = new FeatureCollection();
Object o = TurfUtils.toWgs84(fc); // Works and give an object which respect the FeatureCollection scheme (ie an array of Features) when I print it on the javascript console.
FeatureCollection featureCollection = TurfUtils.toWgs84(fc); // Throw a java.lang.ClassCastException

The Turf library is JsInteroped:

@JsType(isNative = true, namespace = GLOBAL, name = "turf")
public class TurfUtils {

    public static native <T extends GeoJson> T toWgs84(T geojson);
}

When making my FeatureCollection a native JsType, it works but prevent me to use my current constructor, so I'm looking for a way to cast back a javascript object to my JsType. [1]: https://www.rfc-editor.org/rfc/rfc7946

2

There are 2 answers

0
Ignacio Baca On

As Colin explained, you don't have any type to check in a GeoJson object, so you can not use instanceof or other OOP techniques to cast it back to the specific type safety. You must set the type as native=true, name="Object", namespace=GLOBAL and then you can use Js.cast to cast it back as a GeoJson type.

If you want something more OOP, you can use a visitor pattern and hide the "manual type checking" behind this visitor, for example:

import static jsinterop.annotations.JsPackage.GLOBAL;

import javax.annotation.Nullable;
import jsinterop.annotations.JsOverlay;
import jsinterop.annotations.JsType;

@JsType(namespace = GLOBAL, name = "Object", isNative = true)
class GeoJson {
    public String type;
    public final @JsOverlay Type getTypeEnum() { return Type.valueOf(type); }
    public final @JsOverlay void setTypeEnum(Type type) { this.type = type.name(); }

    public static @JsOverlay FeatureCollection featureCollection(Feature... features) {
        FeatureCollection o = new FeatureCollection();
        o.setTypeEnum(Type.FeatureCollection);
        o.features = features;
        return o;
    }

    public static @JsOverlay Feature feature(Geometry geometry) { return feature(null, geometry); }
    public static @JsOverlay Feature feature(@Nullable String featureId, Geometry geometry) {
        Feature o = new Feature();
        o.setTypeEnum(Type.Feature);
        o.id = featureId;
        o.geometry = geometry;
        return o;
    }

    public static @JsOverlay Point point(double x, double y) { return point(new double[] { x, y }); }
    public static @JsOverlay Point point(double[] coordinates) {
        Point o = new Point();
        o.setTypeEnum(Geometry.Type.Point);
        o.coordinates = coordinates;
        return o;
    }

    public static @JsOverlay Polygon polygon(double[][] coordinates) {
        Polygon o = new Polygon();
        o.setTypeEnum(Geometry.Type.Polygon);
        o.coordinates = new double[][][] { coordinates };
        return o;
    }

    public enum Type {Feature, FeatureCollection}

    @JsType(namespace = GLOBAL, name = "Object", isNative = true)
    public static final class Feature extends GeoJson {
        public @Nullable String id;
        public Geometry geometry;
    }

    @JsType(namespace = GLOBAL, name = "Object", isNative = true)
    public static class FeatureCollection extends GeoJson {
        public Feature[] features;
    }

    @JsType(namespace = GLOBAL, name = "Object", isNative = true)
    public static abstract class Geometry {
        public String type;
        public final @JsOverlay Geometry.Type getTypeEnum() { return Geometry.Type.valueOf(type); }
        public final @JsOverlay void setTypeEnum(Geometry.Type type) { this.type = type.name(); }

        public final @JsOverlay <T> T accept(GeometryVisitor<T> fn) { switch (getTypeEnum()) {
            case Point: return fn.point((Point) this);
            case Polygon: return fn.polygon((Polygon) this);
            default: throw new UnsupportedOperationException("unexpected type " + type);
        } }

        public static @JsOverlay @Nullable Point isPoint(@Nullable Geometry g) {
            return g == null ? null : g.accept(new GeometryVisitor<Point>() {
                @Override public Point point(Point g) { return g; }
                @Override public Point polygon(Polygon p) { return null; }
            });
        }

        public static @JsOverlay @Nullable Polygon isPolygon(@Nullable Geometry g) {
            return g == null ? null : g.accept(new GeometryVisitor<Polygon>() {
                @Override public Polygon point(Point g) { return null; }
                @Override public Polygon polygon(Polygon p) { return p; }
            });
        }

        public enum Type {Point, Polygon}
    }

    @JsType(namespace = GLOBAL, name = "Object", isNative = true)
    public static class Point extends Geometry {
        public double[] coordinates;
        public final @JsOverlay double x() { return coordinates[0]; }
        public final @JsOverlay double y() { return coordinates[1]; }
    }

    @JsType(namespace = GLOBAL, name = "Object", isNative = true)
    public static final class Polygon extends Geometry {
        public double[][][] coordinates;
        public final @JsOverlay double[][] shell() { return coordinates[0]; }
    }

    public interface GeometryVisitor<T> {
        T point(Point g);
        T polygon(Polygon p);
    }
}

Example based on this one which also includes jackson annotation so it can be in the server side too.

0
Colin Alworth On

The @JsType and related annotations do not create wrappers that try to understand what you meant to do, but they actually generate JS code that corresponds as closely as possible to what you did do. This means that if you say "I'm making a new non-native JS type, and it will have a constructor defined like this", GWT will say "okay" and do it. And the result will be a type in JS with a constructor, but objects not created with that exact constructor by definition, are not of that type, and you may get an error if you try to treat them as if they were.

Instead, your FeatureCollection should almost certainly be a native type, probably plain Object in the JsPackage.GLOBAL namespace, and instead of a constructor, you should have a factory method.

Alternatively, you could risk using Js.uncheckedCast to say "trust me, this object is more or less the right shape (though it might be the wrong type), just use it as if it were the same type", and as long as GWT has no reason to typecheck further, it will let you get away with it. This is probably suitable for use in your own application code, but with very clear notes about what you are doing and when it will go wrong.


Side note - generally if you have getters and setters in a non-native JsType, you should mark them as @JsProperty instead of marking the private field so - if you made the field final, other JS might assign it later anyway, if you made the getter or setter do some validation or caching, any access from JS would miss that. Remember too that if a type is a JsType it will automatically have all of its public members exported, so you could achieve the same thing here by just removing JsProperty and the getter, and make the field public.