/* * Copyright (C) 2012-2015 DataStax Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.datastax.driver.mapping; import com.datastax.driver.core.CCMTestsSupport; import com.datastax.driver.core.utils.CassandraVersion; import com.datastax.driver.mapping.annotations.*; import com.google.common.base.Objects; import com.google.common.collect.Sets; import org.testng.annotations.AfterMethod; import org.testng.annotations.Test; import java.util.Set; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; /** * Tests for * JAVA-541 Add polymorphism support to object mapper * JAVA-636 Allow @Column annotations on getters/setters as well as fields */ @SuppressWarnings({"unused", "WeakerAccess"}) @CassandraVersion("2.1.0") public class MapperPolymorphismTest extends CCMTestsSupport { Circle circle = new Circle(new Point2D(11, 22), 12.34); Rectangle rectangle = new Rectangle(new Point2D(20, 30), new Point2D(50, 60)); Square square = new Square(new Point2D(20, 30), new Point2D(50, 60)); Sphere sphere = new Sphere(new Point3D(11, 22, 33), 34.56); @Override public void onTestContextInitialized() { execute( "CREATE TYPE point2d (\"X\" int, \"Y\" int)", "CREATE TYPE point3d (\"X\" int, \"Y\" int, \"Z\" int)", "CREATE TABLE circles (circle_id uuid PRIMARY KEY, center2d frozen<point2d>, radius double, tags set<text>)", "CREATE TABLE rectangles (rect_id uuid PRIMARY KEY, bottom_left frozen<point2d>, top_right frozen<point2d>, tags set<text>)", "CREATE TABLE squares (square_id uuid PRIMARY KEY, bottom_left frozen<point2d>, top_right frozen<point2d>, tags set<text>)", "CREATE TABLE spheres (sphere_id uuid PRIMARY KEY, center3d frozen<point3d>, radius double, tags set<text>)"); } @AfterMethod(groups = "short") public void clean() { execute("TRUNCATE circles", "TRUNCATE rectangles", "TRUNCATE squares", "TRUNCATE spheres"); } @UDT(name = "point2d") static class Point2D { // test mix of field and getter - getter should win @Field(name = "wrong") private int x; private int y; // test private constructor + "immutability" private Point2D() { } public Point2D(int x, int y) { this.x = x; this.y = y; } @Field(name = "X", caseSensitive = true) public int getX() { return x; } @Field(name = "Y", caseSensitive = true) public int getY() { return y; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Point2D point2D = (Point2D) o; return getX() == point2D.getX() && getY() == point2D.getY(); } @Override public int hashCode() { return Objects.hashCode(getX(), getY()); } } @UDT(name = "point3d") static class Point3D extends Point2D { private int z; // test private constructor + "immutability" private Point3D() { } public Point3D(int x, int y, int z) { super(x, y); this.z = z; } @Field(name = "Z", caseSensitive = true) public int getZ() { return z; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; Point3D point3D = (Point3D) o; return getZ() == point3D.getZ(); } @Override public int hashCode() { return Objects.hashCode(super.hashCode(), getZ()); } } interface Shape2D { Set<String> getTags(); // test annotation on interface method, should get inherited everywhere @Transient double getArea(); } interface Shape3D { double getVolume(); } static abstract class Shape implements Shape2D { @PartitionKey // annotated field on superclass; annotation will get inherited in all subclasses protected UUID id; protected Set<String> tags; public Shape() { this.id = UUID.randomUUID(); this.tags = Sets.newHashSet("cool", "awesome"); } @Column(name = "wrong") // gets overridden in all subclasses public abstract UUID getId(); public void setId(UUID id) { this.id = id; } @Override public Set<String> getTags() { return tags; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Shape shape = (Shape) o; return Objects.equal(getId(), shape.getId()); } @Override public int hashCode() { return Objects.hashCode(getId()); } } @Table(name = "circles") static class Circle extends Shape { @Column(name = "center2d") // overridden by a getter in Sphere @Frozen protected Point2D center; // tests unusual field name (i.e. does not correspond to getter/setter) // will be considered as a separate "property" with no getter nor setter; // thus needs to be annotated with @Transient @Transient private double _radius; private long writeTime; public Circle() { } public Circle(Point2D center, double radius) { this.center = center; this._radius = radius; } @Column(name = "circle_id") @Override public UUID getId() { return id; } public Point2D getCenter() { return center; } public void setCenter(Point2D center) { this.center = center; } public double getRadius() { return _radius; } // builder-style setter; because the field isn't named in a standard way, // if this setter is not detected the test would fail public Circle setRadius(double radius) { this._radius = radius; return this; } @Override // inherits @Transient public double getArea() { return Math.PI * (Math.pow(getRadius(), 2)); } // tests computed columns - no setter @Computed(value = "writetime(\"radius\")") public long getWriteTime() { return writeTime; } // mismatched setter - getter is declared in Shape public void setTags(Set<String> tags) { this.tags = tags; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; Circle circle = (Circle) o; return Double.compare(circle.getRadius(), getRadius()) == 0 && Objects.equal(getCenter(), circle.getCenter()); } @Override public int hashCode() { return Objects.hashCode(super.hashCode(), getCenter(), getRadius()); } } @Table(name = "rectangles") static class Rectangle extends Shape { private Point2D bottomLeft; private Point2D topRight; public Rectangle() { } public Rectangle(Point2D bottomLeft, Point2D topRight) { this.bottomLeft = bottomLeft; this.topRight = topRight; } @Column(name = "rect_id") @Override public UUID getId() { return id; } @Column(name = "bottom_left") @Frozen public Point2D getBottomLeft() { return bottomLeft; } public void setBottomLeft(Point2D bottomLeft) { this.bottomLeft = bottomLeft; } @Column(name = "top_right") @Frozen public Point2D getTopRight() { return topRight; } public void setTopRight(Point2D topRight) { this.topRight = topRight; } // test annotation in class method @Transient public double getWidth() { return Math.abs(topRight.getX() - bottomLeft.getX()); } @Transient public double getHeight() { return Math.abs(topRight.getY() - bottomLeft.getY()); } @Override // inherits @Transient public double getArea() { return getWidth() * getHeight(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; Rectangle rectangle = (Rectangle) o; return Objects.equal(getBottomLeft(), rectangle.getBottomLeft()) && Objects.equal(getTopRight(), rectangle.getTopRight()); } @Override public int hashCode() { return Objects.hashCode(super.hashCode(), getBottomLeft(), getTopRight()); } } @Table(name = "squares") static class Square extends Rectangle { public Square() { } public Square(Point2D bottomLeft, Point2D topRight) { super(bottomLeft, topRight); assert getHeight() == getWidth(); } @Column(name = "square_id") @Override public UUID getId() { return id; } @Override // inherits @Column and @Frozen public Point2D getBottomLeft() { return super.getBottomLeft(); } @Override // inherits @Transient public double getHeight() { return getWidth(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; Square square = (Square) o; return Objects.equal(getBottomLeft(), square.getBottomLeft()) && Objects.equal(getTopRight(), square.getTopRight()); } } @Table(name = "spheres") static class Sphere extends Circle implements Shape3D { private long writeTime; public Sphere() { } public Sphere(Point3D center, double radius) { this.center = center; } @Column(name = "sphere_id") @Override public UUID getId() { return id; } // overrides field annotation in Circle, // note that the property type is narrowed down to Point3D @Column(name = "center3d") @Frozen @Override public Point3D getCenter() { return (Point3D) center; } @Override public void setCenter(Point2D center) { assert center instanceof Point3D; this.center = center; } // overridden builder-style setter @Override public Sphere setRadius(double radius) { super.setRadius(radius); return this; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; Sphere sphere = (Sphere) o; return Objects.equal(getCenter(), sphere.getCenter()); } @Override public int hashCode() { return Objects.hashCode(super.hashCode(), getCenter()); } // test annotation on implementation @Override @Transient public double getVolume() { return 4d / 3d * Math.PI * Math.pow(getRadius(), 3); } } /** * Validates that fields and methods from a parent class {@link Shape} are inherited for a mapped entity * class {@link Circle}. * <p/> * It also ensures that methods overridden in {@link Circle} are given precedence and that their annotations are * used. In the case of circle, the {@link Circle#getId()} method overrides the parent and specifies a * {@link Column} annotation that will be used instead of the parents annotation ('wrong'). * <p/> * Lastly, it defines another column 'radius' that is defined by having {@link Circle#getRadius} and * {@link Circle#setRadius} properties, while it has a {@link Transient} field '_radius' that should not be pulled * in by the mapper. * * @test_category object_mapper * @jira_ticket JAVA-541, JAVA-636 */ @Test(groups = "short") public void should_save_and_retrieve_circle() throws Exception { MappingManager mappingManager = new MappingManager(session()); Mapper<Circle> circleMapper = mappingManager.mapper(Circle.class); circleMapper.save(circle); assertThat(circleMapper.get(circle.getId())).isEqualTo(circle); } /** * Validates that property methods such as {@link Rectangle#getWidth} and {@link Rectangle#getHeight} are not * considered as they are marked as {@link Transient}. It also ensures that overridden method * {@link Rectangle#getArea} inherits its parent's {@link Transient} annotation. * * @test_category object_mapper * @jira_ticket JAVA-541, JAVA-636 */ @Test(groups = "short") public void should_save_and_retrieve_rectangle() throws Exception { MappingManager mappingManager = new MappingManager(session()); Mapper<Rectangle> rectangleMapper = mappingManager.mapper(Rectangle.class); rectangleMapper.save(rectangle); assertThat(rectangleMapper.get(rectangle.getId())).isEqualTo(rectangle); } /** * Validates that {@link Square} inherits its properties from its parent class {@link Rectangle} and its parent's * class {@link Shape} and that its methods {@link Square#getBottomLeft} and {@link Square#getHeight} inherit * annotations from its parent. * * @test_category object_mapper * @jira_ticket JAVA-541, JAVA-636 */ @Test(groups = "short") public void should_save_and_retrieve_square() throws Exception { MappingManager mappingManager = new MappingManager(session()); Mapper<Square> squareMapper = mappingManager.mapper(Square.class); squareMapper.save(square); assertThat(squareMapper.get(square.getId())).isEqualTo(square); } /** * Validates that {@link Sphere} inherits its properties from its parent class {@link Circle} and most importantly * can specialize the type of {@link Sphere#center} to a {@link Point3D} instead of a {@link Point2D}. * * @test_category object_mapper * @jira_ticket JAVA-541, JAVA-636 */ @Test(groups = "short") public void should_save_and_retrieve_sphere() throws Exception { MappingManager mappingManager = new MappingManager(session()); Mapper<Sphere> sphereMapper = mappingManager.mapper(Sphere.class); sphereMapper.save(sphere); assertThat(sphereMapper.get(sphere.getId())).isEqualTo(sphere); } }