content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you 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.oss.driver.mapper;
  
  import static com.datastax.oss.driver.assertions.Assertions.assertThat;
  
  import com.datastax.oss.driver.api.core.CqlIdentifier;
  import com.datastax.oss.driver.api.core.CqlSession;
  import com.datastax.oss.driver.api.core.cql.BoundStatement;
  import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder;
  import com.datastax.oss.driver.api.core.cql.PreparedStatement;
  import com.datastax.oss.driver.api.core.cql.ResultSet;
  import com.datastax.oss.driver.api.core.cql.SimpleStatement;
  import com.datastax.oss.driver.api.mapper.annotations.Computed;
  import com.datastax.oss.driver.api.mapper.annotations.CqlName;
  import com.datastax.oss.driver.api.mapper.annotations.Dao;
  import com.datastax.oss.driver.api.mapper.annotations.DaoFactory;
  import com.datastax.oss.driver.api.mapper.annotations.DaoKeyspace;
  import com.datastax.oss.driver.api.mapper.annotations.DaoTable;
  import com.datastax.oss.driver.api.mapper.annotations.DefaultNullSavingStrategy;
  import com.datastax.oss.driver.api.mapper.annotations.Delete;
  import com.datastax.oss.driver.api.mapper.annotations.Entity;
  import com.datastax.oss.driver.api.mapper.annotations.GetEntity;
  import com.datastax.oss.driver.api.mapper.annotations.HierarchyScanStrategy;
  import com.datastax.oss.driver.api.mapper.annotations.Insert;
  import com.datastax.oss.driver.api.mapper.annotations.Mapper;
  import com.datastax.oss.driver.api.mapper.annotations.PartitionKey;
  import com.datastax.oss.driver.api.mapper.annotations.Query;
  import com.datastax.oss.driver.api.mapper.annotations.Select;
  import com.datastax.oss.driver.api.mapper.annotations.SetEntity;
  import com.datastax.oss.driver.api.mapper.annotations.Transient;
  import com.datastax.oss.driver.api.mapper.annotations.Update;
  import com.datastax.oss.driver.api.mapper.entity.saving.NullSavingStrategy;
  import com.datastax.oss.driver.api.testinfra.ccm.CcmRule;
  import com.datastax.oss.driver.api.testinfra.ccm.SchemaChangeSynchronizer;
  import com.datastax.oss.driver.api.testinfra.session.SessionRule;
  import com.datastax.oss.driver.categories.ParallelizableTests;
  import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList;
  import com.datastax.oss.driver.shaded.guava.common.collect.Sets;
  import com.tngtech.java.junit.dataprovider.DataProvider;
  import com.tngtech.java.junit.dataprovider.DataProviderRunner;
  import com.tngtech.java.junit.dataprovider.UseDataProvider;
  import java.util.Objects;
  import java.util.Set;
  import java.util.UUID;
  import java.util.concurrent.CompletableFuture;
  import java.util.function.Consumer;
  import java.util.function.Function;
  import org.junit.BeforeClass;
  import org.junit.ClassRule;
  import org.junit.Test;
  import org.junit.experimental.categories.Category;
  import org.junit.rules.RuleChain;
  import org.junit.rules.TestRule;
  import org.junit.runner.RunWith;
  
  @Category(ParallelizableTests.class)
  @RunWith(DataProviderRunner.class)
  public class EntityPolymorphismIT {
    private static final CcmRule CCM_RULE = CcmRule.getInstance();
  
    private static final SessionRule<CqlSession> SESSION_RULE = SessionRule.builder(CCM_RULE).build();
  
    @ClassRule
    public static final TestRule CHAIN = RuleChain.outerRule(CCM_RULE).around(SESSION_RULE);
  
    private static TestMapper mapper;
  
    @BeforeClass
    public static void setup() {
      CqlSession session = SESSION_RULE.session();
      SchemaChangeSynchronizer.withLock(
          () -> {
            for (String query :
                ImmutableList.of(
                    "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>)",
                    "CREATE TABLE devices (device_id uuid PRIMARY KEY, name text)",
                    "CREATE TABLE tracked_devices (device_id uuid PRIMARY KEY, name text, location text)",
                    "CREATE TABLE simple_devices (id uuid PRIMARY KEY, in_use boolean)")) {
              session.execute(
                  SimpleStatement.builder(query)
                      .setExecutionProfile(SESSION_RULE.slowProfile())
                      .build());
            }
          });
      mapper = new EntityPolymorphismIT_TestMapperBuilder(session).build();
    }
  
    // define parent interface with dao methods.
    @DefaultNullSavingStrategy(NullSavingStrategy.SET_TO_NULL)
    interface BaseDao<T> {
      @Insert
      void save(T t);
  
      @Select
      T findById(UUID id);
  
      @Delete
      void delete(T t);
  
      @SetEntity
      void bind(T t, BoundStatementBuilder builder);
  
      @GetEntity
      T one(ResultSet result);
  
      @Update
      void update(T t);
    }
  
    // another parent interface with dao methods
    interface WriteTimeDao<Y extends WriteTimeProvider> extends BaseDao<Y> {
      @Insert(timestamp = ":writeTime")
      void saveWithTime(Y y, long writeTime);
    }
  
    @Dao
    interface RectangleDao extends BaseDao<Rectangle> {}
  
    // Define an intermediate interface with same type variable name to ensure
    // this doesn't cause any issue in code generation.
    interface ArbitraryInterface<Y extends Number> {
      default long increment(Y input) {
        return input.longValue() + 1;
      }
    }
  
    @Dao
    interface SquareDao extends WriteTimeDao<Square>, ArbitraryInterface<Float> {}
  
    @Dao
    interface CircleDao extends WriteTimeDao<Circle> {}
  
    @Dao
    interface SphereDao extends WriteTimeDao<Sphere> {}
  
    interface NamedDeviceDao<Y extends Device> extends BaseDao<Y> {
      @Query("UPDATE ${qualifiedTableId} SET name = :name WHERE device_id = :id")
      void updateName(String name, UUID id);
  
      @Query("SELECT * FROM ${qualifiedTableId} WHERE device_id = :id")
      CompletableFuture<Y> findByIdQueryAsync(UUID id);
    }
  
    @Dao
    interface DeviceDao extends NamedDeviceDao<Device> {}
  
    @Dao
    interface TrackedDeviceDao extends NamedDeviceDao<TrackedDevice> {}
  
    @Dao
    interface SimpleDeviceDao extends BaseDao<SimpleDevice> {}
  
    @Mapper
    public interface TestMapper {
      @DaoFactory
      CircleDao circleDao(@DaoKeyspace CqlIdentifier keyspace);
  
      @DaoFactory
      RectangleDao rectangleDao(@DaoKeyspace CqlIdentifier keyspace);
  
      @DaoFactory
      SquareDao squareDao(@DaoKeyspace CqlIdentifier keyspace);
  
      @DaoFactory
      SphereDao sphereDao(@DaoKeyspace CqlIdentifier keyspace);
  
      @DaoFactory
      DeviceDao deviceDao(@DaoKeyspace CqlIdentifier keyspace, @DaoTable CqlIdentifier table);
  
      @DaoFactory
      TrackedDeviceDao trackedDeviceDao(
          @DaoKeyspace CqlIdentifier keyspace, @DaoTable CqlIdentifier table);
  
      @DaoFactory
      SimpleDeviceDao simpleDeviceDao(@DaoKeyspace CqlIdentifier keyspace);
    }
  
    @DataProvider
    public static Object[][] setAndGetProvider() {
      Function<CqlIdentifier, CircleDao> circleDao = keyspace -> mapper.circleDao(keyspace);
      Function<CqlIdentifier, RectangleDao> rectangleDao = keyspace -> mapper.rectangleDao(keyspace);
      Function<CqlIdentifier, SquareDao> squareDao = keyspace -> mapper.squareDao(keyspace);
      Function<CqlIdentifier, SphereDao> sphereDao = keyspace -> mapper.sphereDao(keyspace);
      return new Object[][] {
        {
          new Rectangle(new Point2D(20, 30), new Point2D(50, 60)),
          rectangleDao,
          (Consumer<Rectangle>) (Rectangle r) -> r.setTopRight(new Point2D(21, 31)),
          SimpleStatement.newInstance(
              "insert into rectangles (rect_id, bottom_left, top_right, "
                  + "tags) values (?, ?, ?, ?)"),
          SimpleStatement.newInstance("select * from rectangles where rect_id = :id limit 1")
        },
        {
          new Circle(new Point2D(11, 22), 12.34),
          circleDao,
          (Consumer<Circle>) (Circle c) -> c.setRadius(13.33),
          SimpleStatement.newInstance(
              "insert into circles (circle_id, center2d, radius, tags) " + "values (?, ?, ?, ?)"),
          SimpleStatement.newInstance(
              "select circle_id, center2d, radius, tags, writetime(radius) "
                  + "as write_time from circles where circle_id = :id limit 1")
        },
        {
          new Square(new Point2D(20, 30), new Point2D(50, 60)),
          squareDao,
          (Consumer<Square>) (Square s) -> s.setBottomLeft(new Point2D(10, 20)),
          SimpleStatement.newInstance(
              "insert into squares (square_id, bottom_left, top_right, "
                  + "tags) values (?, ?, ?, ?)"),
          SimpleStatement.newInstance(
              "select square_id, bottom_left, top_right, tags, writetime"
                  + "(bottom_left) as write_time from squares where square_id = :id limit 1")
        },
        {
          new Sphere(new Point3D(11, 22, 33), 34.56),
          sphereDao,
          (Consumer<Sphere>) (Sphere s) -> s.setCenter(new Point3D(10, 20, 30)),
          SimpleStatement.newInstance(
              "insert into spheres (sphere_id, center3d, radius, tags) " + "values (?, ?, ?, ?)"),
          SimpleStatement.newInstance(
              "select sphere_id, center3d, radius, tags, writetime(radius) "
                  + "as write_time from spheres where sphere_id = :id limit 1")
        },
      };
    }
  
    @UseDataProvider("setAndGetProvider")
    @Test
    public <T extends Shape> void should_set_and_get_entity_then_update_then_delete(
        T t,
        Function<CqlIdentifier, BaseDao<T>> daoProvider,
        Consumer<T> updater,
        SimpleStatement insertStatement,
        SimpleStatement selectStatement) {
      BaseDao<T> dao = daoProvider.apply(SESSION_RULE.keyspace());
      CqlSession session = SESSION_RULE.session();
      PreparedStatement prepared = session.prepare(insertStatement);
  
      BoundStatementBuilder bs = prepared.boundStatementBuilder();
      dao.bind(t, bs);
  
      session.execute(bs.build());
  
      PreparedStatement selectPrepared = session.prepare(selectStatement);
      BoundStatement selectBs = selectPrepared.bind(t.getId());
      T retrieved = dao.one(session.execute(selectBs));
      assertThat(retrieved).isEqualTo(t);
  
      // update value
      updater.accept(t);
      dao.update(t);
  
      // retrieve value to ensure update worked correctly.
      assertThat(dao.one(session.execute(selectBs))).isEqualTo(t);
  
      // delete value and ensure it's gone
      dao.delete(t);
      assertThat(dao.one(session.execute(selectBs))).isNull();
    }
  
    @Test
    public void should_save_and_retrieve_circle() {
      // verifies the inheritance behavior around Circle.
      // * CqlName("circle_id") on getId renames id property to circle_id
      // * annotations, but these are primarily used for
      //   verifying inheritance behavior in Sphere.
      // * verifies writeTime is set.
      CircleDao dao = mapper.circleDao(SESSION_RULE.keyspace());
  
      long writeTime = System.currentTimeMillis() - 1000;
      Circle circle = new Circle(new Point2D(11, 22), 12.34);
      dao.saveWithTime(circle, writeTime);
  
      Circle retrievedCircle = dao.findById(circle.getId());
      assertThat(retrievedCircle).isEqualTo(circle);
      assertThat(retrievedCircle.getWriteTime()).isEqualTo(writeTime);
    }
  
    @Test
    public void should_save_and_retrieve_rectangle() {
      // verifies the inheritance behavior around Rectangle:
      // * CqlName("rect_id") on getId renames id property to rect_id
      // * annotations work, but these are primarily used for
      //   verifying inheritance behavior in Square.
      RectangleDao dao = mapper.rectangleDao(SESSION_RULE.keyspace());
  
      Rectangle rectangle = new Rectangle(new Point2D(20, 30), new Point2D(50, 60));
      dao.save(rectangle);
  
      assertThat(dao.findById(rectangle.getId())).isEqualTo(rectangle);
    }
  
    @Test
    public void should_save_and_retrieve_square() {
      // verifies the inheritance behavior around Square:
      // * CqlName("square_id") on getId renames id property to square_id
      // * height remains transient even though we define field/getter/setter
      // * getBottomLeft() retains CqlName from parent.
      // * verifies writeTime is set.
      SquareDao dao = mapper.squareDao(SESSION_RULE.keyspace());
  
      long writeTime = System.currentTimeMillis() - 1000;
      Square square = new Square(new Point2D(20, 30), new Point2D(50, 60));
      dao.saveWithTime(square, writeTime);
  
      Square retrievedSquare = dao.findById(square.getId());
      assertThat(retrievedSquare).isEqualTo(square);
      assertThat(retrievedSquare.getWriteTime()).isEqualTo(writeTime);
    }
  
    @Test
    public void should_save_and_retrieve_sphere() {
      // verifies the inheritance behavior around Circle:
      // * @CqlName("sphere_id") on getId renames id property to sphere_id
      // * @CqlName("center3d") on getCenter renames center property from center2d
      //   which was renamed on the parent field for Circle
      // * That getCenter returns Point3D influences get/set behavior to use Point3D
      // * Override setRadius to return Sphere causes no issues.
      // * Interface method getVolume() is skipped because no field exists.
      // * WriteTime is inherited, so queried and set.
      SphereDao dao = mapper.sphereDao(SESSION_RULE.keyspace());
  
      long writeTime = System.currentTimeMillis() - 1000;
      Sphere sphere = new Sphere(new Point3D(11, 22, 33), 34.56);
      dao.saveWithTime(sphere, writeTime);
  
      Sphere retrievedSphere = dao.findById(sphere.getId());
      assertThat(retrievedSphere).isEqualTo(sphere);
      assertThat(retrievedSphere.getWriteTime()).isEqualTo(writeTime);
    }
  
    @Test
    public void should_save_and_retrieve_device() throws Exception {
      // verifies the hierarchy scanner behavior around Device:
      // * by virtue of Assert setting highestAncestor to Asset.class, location property from
      //   LocatableItem should not be included
      DeviceDao dao = mapper.deviceDao(SESSION_RULE.keyspace(), CqlIdentifier.fromCql("devices"));
  
      // save should be successful as location property omitted.
      Device device = new Device("my device", "New York");
      dao.save(device);
  
      Device retrievedDevice = dao.findById(device.getId());
      assertThat(retrievedDevice.getId()).isEqualTo(device.getId());
      assertThat(retrievedDevice.getName()).isEqualTo(device.getName());
      // location should be null.
      assertThat(retrievedDevice.getLocation()).isNull();
  
      // should be able to use @Query with update
      String name = "my new name";
      dao.updateName(name, device.getId());
  
      // should be able to use @Query returning Entity and name should be applied.
      retrievedDevice = dao.findByIdQueryAsync(device.getId()).get();
      assertThat(retrievedDevice.getName()).isEqualTo(name);
    }
  
    @Test
    public void should_save_and_retrieve_tracked_device() throws Exception {
      // verifies the hierarchy scanner behavior around TrackedDevice:
      // * Since TrackedDevice defines a default @HierarchyScanStrategy it should
      //   include LocatableItem's location property, even though Asset defines
      //   a strategy that excludes it.
      TrackedDeviceDao dao =
          mapper.trackedDeviceDao(SESSION_RULE.keyspace(), CqlIdentifier.fromCql("tracked_devices"));
  
      TrackedDevice device = new TrackedDevice("my device", "New York");
      dao.save(device);
  
      // location property should be present, thus should equal saved item.
      TrackedDevice retrievedDevice = dao.findById(device.getId());
      assertThat(retrievedDevice).isEqualTo(device);
  
      // should be able to use @Query with update
      String name = "my new name";
      dao.updateName(name, device.getId());
  
      // should be able to use @Query returning Entity and name should be applied.
      retrievedDevice = dao.findByIdQueryAsync(device.getId()).get();
      assertThat(retrievedDevice.getName()).isEqualTo(name);
    }
  
    @Test
    public void should_save_and_retrieve_simple_device() {
      // verifies the hierarchy scanner behavior around SimpleDevice:
      // * Since SimpleDevice defines a @HierarchyScanStrategy that prevents
      //   scanning of ancestors, only its properties (id, inUse) should be included.
      SimpleDeviceDao dao = mapper.simpleDeviceDao(SESSION_RULE.keyspace());
  
      SimpleDevice device = new SimpleDevice(true);
      dao.save(device);
  
      SimpleDevice retrievedDevice = dao.findById(device.getId());
      assertThat(retrievedDevice.getId()).isEqualTo(device.getId());
      assertThat(retrievedDevice.getInUse()).isEqualTo(device.getInUse());
      // location and name should be null
      assertThat(retrievedDevice.getLocation()).isNull();
      assertThat(retrievedDevice.getName()).isNull();
    }
  
    @Entity
    static class Point2D {
      private int x;
      private int y;
  
      public Point2D() {}
  
      public Point2D(int x, int y) {
        this.x = x;
        this.y = y;
      }
  
      @CqlName("\"X\"")
      public int getX() {
        return x;
      }
  
      public void setX(int x) {
        this.x = x;
      }
  
      @CqlName("\"Y\"")
      public int getY() {
        return y;
      }
  
      public void setY(int y) {
        this.y = y;
      }
  
      @Override
      public boolean equals(Object other) {
        if (this == other) return true;
        else if (other instanceof Point2D) {
          Point2D that = (Point2D) other;
          return this.x == that.x && this.y == that.y;
        } else {
          return false;
        }
      }
  
      @Override
      public int hashCode() {
        return Objects.hash(x, y);
      }
    }
  
    @Entity
    static class Point3D extends Point2D {
      private int z;
  
      public Point3D() {}
  
      public Point3D(int x, int y, int z) {
        super(x, y);
        this.z = z;
      }
  
      @CqlName("\"Z\"")
      public int getZ() {
        return z;
      }
  
      public void setZ(int z) {
        this.z = z;
      }
  
      @Override
      public boolean equals(Object other) {
        if (this == other) {
          return true;
        } else if (other instanceof Point3D) {
          Point3D that = (Point3D) other;
          return super.equals(that) && this.z == that.z;
        } else {
          return false;
        }
      }
  
      @Override
      public int hashCode() {
        return Objects.hash(super.hashCode(), z);
      }
    }
  
    interface Shape2D {
      Set<String> getTags();
  
      // test annotation on interface method, should get inherited everywhere
      @Transient
      double getArea();
    }
  
    interface Shape3D {
      double getVolume();
    }
  
    abstract static class Shape implements Shape2D {
  
      @PartitionKey // annotated field on superclass; annotation will get inherited in all subclasses
      protected UUID id;
  
      protected Set<String> tags;
  
      protected String location;
  
      public Shape() {
        this.id = UUID.randomUUID();
        this.tags = Sets.newHashSet("cool", "awesome");
      }
  
      @CqlName("wrong")
      public abstract UUID getId();
  
      public void setId(UUID id) {
        this.id = id;
      }
  
      @Override
      public Set<String> getTags() {
        return tags;
      }
  
      public void setTags(Set<String> tags) {
        this.tags = tags;
      }
  
      @Override
      public boolean equals(Object other) {
        if (this == other) {
          return true;
        } else if (other instanceof Shape) {
          Shape that = (Shape) other;
          return Objects.equals(id, that.id) && Objects.equals(tags, that.tags);
        } else {
          return false;
        }
      }
  
      @Override
      public int hashCode() {
        return Objects.hash(id, tags);
      }
    }
  
    interface WriteTimeProvider {
      long getWriteTime();
    }
  
    @CqlName("circles")
    @Entity
    static class Circle extends Shape implements WriteTimeProvider {
  
      @CqlName("center2d")
      protected Point2D center;
  
      protected double radius;
  
      private long writeTime;
  
      public Circle() {}
  
      public Circle(Point2D center, double radius) {
        super();
        this.center = center;
        this.radius = radius;
      }
  
      @Override
      @CqlName("circle_id")
      public UUID getId() {
        return id;
      }
  
      @Override
      public double getArea() {
        return Math.PI * Math.pow(getRadius(), 2);
      }
  
      public double getRadius() {
        return this.radius;
      }
  
      public Circle setRadius(double radius) {
        this.radius = radius;
        return this;
      }
  
      @Computed("writetime(radius)")
      @Override
      public long getWriteTime() {
        return writeTime;
      }
  
      public void setWriteTime(long writeTime) {
        this.writeTime = writeTime;
      }
  
      public Point2D getCenter() {
        return center;
      }
  
      public void setCenter(Point2D center) {
        this.center = center;
      }
  
      @Override
      public boolean equals(Object other) {
        if (this == other) {
          return true;
        } else if (other instanceof Circle) {
          Circle that = (Circle) other;
          return super.equals(that)
              && Double.compare(that.radius, radius) == 0
              && center.equals(that.center);
        } else {
          return false;
        }
      }
  
      @Override
      public int hashCode() {
        return Objects.hash(super.hashCode(), center, radius);
      }
    }
  
    @CqlName("rectangles")
    @Entity
    static class Rectangle extends Shape {
      private Point2D bottomLeft;
      private Point2D topRight;
  
      public Rectangle() {}
  
      public Rectangle(Point2D bottomLeft, Point2D topRight) {
        super();
        this.bottomLeft = bottomLeft;
        this.topRight = topRight;
      }
  
      @CqlName("rect_id")
      @Override
      public UUID getId() {
        return id;
      }
  
      @CqlName("bottom_left")
      public Point2D getBottomLeft() {
        return bottomLeft;
      }
  
      public void setBottomLeft(Point2D bottomLeft) {
        this.bottomLeft = bottomLeft;
      }
  
      @CqlName("top_right")
      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
      public double getArea() {
        return getWidth() * getHeight();
      }
  
      @Override
      public boolean equals(Object other) {
        if (this == other) {
          return true;
        } else if (other instanceof Rectangle) {
          Rectangle that = (Rectangle) other;
          return super.equals(that)
              && bottomLeft.equals(that.bottomLeft)
              && topRight.equals(that.topRight);
        } else {
          return false;
        }
      }
  
      @Override
      public int hashCode() {
        return Objects.hash(super.hashCode(), bottomLeft, topRight);
      }
    }
  
    @CqlName("squares")
    @Entity
    static class Square extends Rectangle implements WriteTimeProvider {
  
      @Computed("writetime(bottom_left)")
      private long writeTime;
  
      public Square() {}
  
      public Square(Point2D bottomLeft, Point2D topRight) {
        super(bottomLeft, topRight);
        assert getHeight() == getWidth();
      }
  
      @CqlName("square_id")
      @Override
      public UUID getId() {
        return id;
      }
  
      @Override
      // inherits @CqlName
      public Point2D getBottomLeft() {
        return super.getBottomLeft();
      }
  
      @Override
      // inherits @Transient
      public double getHeight() {
        return getWidth();
      }
  
      public void setHeight(Point2D height) {
        throw new IllegalArgumentException("This method should never be called");
      }
  
      @Override
      public long getWriteTime() {
        return writeTime;
      }
  
      public void setWriteTime(long writeTime) {
        this.writeTime = writeTime;
      }
    }
  
    @CqlName("spheres")
    @Entity
    static class Sphere extends Circle implements Shape3D {
  
      // ignored field
      private long writeTime;
  
      public Sphere() {}
  
      public Sphere(Point3D center, double radius) {
        super(center, radius);
      }
  
      @CqlName("sphere_id")
      @Override
      public UUID getId() {
        return id;
      }
  
      // overrides field annotation in Circle,
      // note that the property type is narrowed down to Point3D
      @CqlName("center3d")
      @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 double getVolume() {
        return 4d / 3d * Math.PI * Math.pow(getRadius(), 3);
      }
  
      @Override
      public boolean equals(Object other) {
        if (this == other) {
          return true;
        } else if (other instanceof Sphere) {
          Sphere that = (Sphere) other;
          return super.equals(that) && writeTime == that.writeTime;
        } else {
          return false;
        }
      }
  
      @Override
      public int hashCode() {
        return Objects.hash(super.hashCode(), writeTime);
      }
    }
  
    abstract static class LocatableItem {
  
      protected String location;
  
      LocatableItem() {}
  
      LocatableItem(String location) {
        this.location = location;
      }
  
      public String getLocation() {
        return location;
      }
  
      public void setLocation(String location) {
        this.location = location;
      }
  
      @Override
      public boolean equals(Object other) {
        if (this == other) {
          return true;
        } else if (other instanceof LocatableItem) {
          LocatableItem that = (LocatableItem) other;
          return Objects.equals(this.location, that.location);
        } else {
          return false;
        }
      }
  
      @Override
      public int hashCode() {
        return Objects.hash(location);
      }
    }
  
    // define strategy that stops scanning at Asset, meaning LocatableItem's location will
    // not be considered a property.
    @HierarchyScanStrategy(highestAncestor = Asset.class, includeHighestAncestor = true)
    abstract static class Asset extends LocatableItem {
      protected String name;
  
      Asset() {}
  
      Asset(String name, String location) {
        super(location);
        this.name = name;
      }
  
      public String getName() {
        return name;
      }
  
      public void setName(String name) {
        this.name = name;
      }
  
      @Override
      public boolean equals(Object other) {
        if (this == other) {
          return true;
        } else if (other instanceof Asset) {
          Asset that = (Asset) other;
          return super.equals(that) && Objects.equals(this.name, that.name);
        } else {
          return false;
        }
      }
  
      @Override
      public int hashCode() {
        return Objects.hash(super.hashCode(), name);
      }
    }
  
    // should inherit scan strategy from Asset, thus location will not be present.
    @Entity
    @CqlName("devices")
    static class Device extends Asset {
  
      @PartitionKey protected UUID id;
  
      Device() {}
  
      Device(String name, String location) {
        super(name, location);
        this.id = UUID.randomUUID();
      }
  
      // rename to device_id, if Device not included in scanning, this won't be used.
      @CqlName("device_id")
      public UUID getId() {
        return id;
      }
  
      public void setId(UUID id) {
        this.id = id;
      }
  
      @Override
      public boolean equals(Object other) {
        if (this == other) {
          return true;
        } else if (other instanceof Device) {
          Device that = (Device) other;
          return super.equals(that) && this.id.equals(that.id);
        } else {
          return false;
        }
      }
  
      @Override
      public int hashCode() {
        return Objects.hash(super.hashCode(), id);
      }
    }
  
    // use default strategy, which should override Assert's strategy, and thus location
    // should be considered a property.
    @HierarchyScanStrategy
    @Entity
    @CqlName("tracked_devices")
    static class TrackedDevice extends Device {
      TrackedDevice() {}
  
      TrackedDevice(String name, String location) {
        super(name, location);
      }
    }
  
    // do not scan ancestors, so only id and inUse should be considered properties.
    @HierarchyScanStrategy(scanAncestors = false)
    @Entity
    @CqlName("simple_devices")
    static class SimpleDevice extends Device {
      boolean inUse;
  
      // suppress error prone warning as we are doing this with intent
      @SuppressWarnings("HidingField")
      @PartitionKey
      private UUID id;
  
      SimpleDevice() {}
  
      SimpleDevice(boolean inUse) {
        super(null, null);
        this.id = UUID.randomUUID();
        this.inUse = inUse;
      }
  
      public boolean getInUse() {
        return inUse;
      }
  
      public void setInUse(boolean inUse) {
        this.inUse = inUse;
      }
  
      @Override
      public UUID getId() {
        return id;
      }
  
      @Override
      public void setId(UUID id) {
        this.id = id;
      }
  
      @Override
      public boolean equals(Object other) {
        if (this == other) {
          return true;
        } else if (other instanceof SimpleDevice) {
          SimpleDevice that = (SimpleDevice) other;
          return super.equals(that) && this.inUse == that.inUse && this.id.equals(that.id);
        } else {
          return false;
        }
      }
  
      @Override
      public int hashCode() {
        return Objects.hash(super.hashCode(), inUse, id);
      }
    }
  }
  