/*
* 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);
}
}