Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DRAFT - Support comparable entity fields and comparison DSL #88

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 64 additions & 28 deletions main/src/main/java/com/kenshoo/pl/entity/AbstractEntityType.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,55 +38,87 @@ protected AbstractEntityType(String name) {
this.name = name;
}

protected <T> EntityField<E, T> field(TableField<Record, T> tableField) {
protected <T> MutableEntityField<E, T> field(final TableField<Record, T> tableField) {
return field(tableField, IdentityValueConverter.getInstance(tableField.getType()));
}

protected <T, DBT> EntityField<E, T> field(TableField<Record, DBT> tableField, ValueConverter<T, DBT> valueConverter) {
protected <T, DBT> MutableEntityField<E, T> field(final TableField<Record, DBT> tableField, final ValueConverter<T, DBT> valueConverter) {
return field(tableField, valueConverter, Objects::equals);
}

protected <T, DBT> EntityField<E, T> field(TableField<Record, DBT> tableField, ValueConverter<T, DBT> valueConverter, ValueConverter<T, String> stringValueConverter) {
protected <T, DBT> MutableEntityField<E, T> field(final TableField<Record, DBT> tableField,
final ValueConverter<T, DBT> valueConverter,
final ValueConverter<T, String> stringValueConverter) {
return field(tableField, valueConverter, stringValueConverter, Objects::equals);
}

protected <T> EntityField<E, T> field(TableField<Record, T> tableField, EntityValueEqualityFunction<T> valueEqualityFunction) {
return field(tableField, IdentityValueConverter.getInstance(tableField.getType()), createStringValueConverter(tableField.getType()), valueEqualityFunction);
protected <T> MutableEntityField<E, T> field(final TableField<Record, T> tableField,
final EntityValueEqualityFunction<T> valueEqualityFunction) {
return field(tableField,
IdentityValueConverter.getInstance(tableField.getType()),
createStringValueConverter(tableField.getType()),
valueEqualityFunction);
}

protected <T, DBT> EntityField<E, T> field(TableField<Record, DBT> tableField, ValueConverter<T, DBT> valueConverter, EntityValueEqualityFunction<T> valueEqualityFunction) {
protected <T, DBT> MutableEntityField<E, T> field(final TableField<Record, DBT> tableField,
final ValueConverter<T, DBT> valueConverter,
final EntityValueEqualityFunction<T> valueEqualityFunction) {
return field(tableField, valueConverter, createStringValueConverter(valueConverter.getValueClass()), valueEqualityFunction);
}

protected <T, DBT> EntityField<E, T> field(TableField<Record, DBT> tableField, ValueConverter<T, DBT> valueConverter,
ValueConverter<T, String> stringValueConverter, EntityValueEqualityFunction<T> valueEqualityFunction) {
return addField(new EntityFieldImpl<>(this, new SimpleEntityFieldDbAdapter<>(tableField, valueConverter), stringValueConverter, valueEqualityFunction));
}

protected <T> EntityField<E, T> field(EntityFieldDbAdapter<T> dbAdapter, ValueConverter<T, String> stringValueConverter) {
return addField(new EntityFieldImpl<>(this, dbAdapter, stringValueConverter, Objects::equals));
}

protected <T, T1> EntityField<E, T> virtualField(EntityField<E, T1> field1, Function<T1, T> translator,
ValueConverter<T, String> stringValueConverter, EntityValueEqualityFunction<T> valueEqualityFunction) {
protected <T, DBT> MutableEntityField<E, T> field(final TableField<Record, DBT> tableField,
final ValueConverter<T, DBT> valueConverter,
final ValueConverter<T, String> stringValueConverter,
final EntityValueEqualityFunction<T> valueEqualityFunction) {
return addField(
EntityFieldImpl.<E, T>builder(this)
.withDbAdapter(new SimpleEntityFieldDbAdapter<>(tableField, valueConverter))
.withStringValueConverter(stringValueConverter)
.withValueEqualityFunction(valueEqualityFunction)
.build()
);
}

protected <T> MutableEntityField<E, T> field(EntityFieldDbAdapter<T> dbAdapter, ValueConverter<T, String> stringValueConverter) {
return addField(
EntityFieldImpl.<E, T>builder(this)
.withDbAdapter(dbAdapter)
.withStringValueConverter(stringValueConverter)
.withValueEqualityFunction(Objects::equals)
.build()
);
}

protected <T, T1> MutableEntityField<E, T> virtualField(final EntityField<E, T1> field1,
final Function<T1, T> translator,
final ValueConverter<T, String> stringValueConverter,
final EntityValueEqualityFunction<T> valueEqualityFunction) {
return virtualField(new VirtualEntityFieldDbAdapter<>(field1.getDbAdapter(), translator), stringValueConverter, valueEqualityFunction);
}

protected <T> EntityField<E, T> virtualField(DataTable table,
ValueConverter<T, String> stringValueConverter,
EntityValueEqualityFunction<T> valueEqualityFunction) {
protected <T> MutableEntityField<E, T> virtualField(final DataTable table,
final ValueConverter<T, String> stringValueConverter,
final EntityValueEqualityFunction<T> valueEqualityFunction) {
return virtualField(new EmptyVirtualEntityFieldDbAdapter<>(table), stringValueConverter, valueEqualityFunction);
}

protected <T, T1, T2> EntityField<E, T> virtualField(EntityField<E, T1> field1, EntityField<E, T2> field2, BiFunction<T1, T2, T> combiner,
ValueConverter<T, String> stringValueConverter, EntityValueEqualityFunction<T> valueEqualityFunction) {
protected <T, T1, T2> MutableEntityField<E, T> virtualField(final EntityField<E, T1> field1,
final EntityField<E, T2> field2,
final BiFunction<T1, T2, T> combiner,
final ValueConverter<T, String> stringValueConverter,
final EntityValueEqualityFunction<T> valueEqualityFunction) {
return virtualField(new VirtualEntityFieldDbAdapter2<>(field1.getDbAdapter(), field2.getDbAdapter(), combiner), stringValueConverter, valueEqualityFunction);
}

private <T> EntityField<E, T> virtualField(EntityFieldDbAdapter<T> entityFieldDbAdapter,
ValueConverter<T, String> stringValueConverter,
EntityValueEqualityFunction<T> valueEqualityFunction) {
return addField(new VirtualEntityFieldImpl<>(this, entityFieldDbAdapter, stringValueConverter, valueEqualityFunction));
private <T> MutableEntityField<E, T> virtualField(final EntityFieldDbAdapter<T> entityFieldDbAdapter,
final ValueConverter<T, String> stringValueConverter,
final EntityValueEqualityFunction<T> valueEqualityFunction) {
return addField(
VirtualEntityFieldImpl.<E, T>builder(this)
.withDbAdapter(entityFieldDbAdapter)
.withStringValueConverter(stringValueConverter)
.withValueEqualityFunction(valueEqualityFunction)
.build());
}

private static <T> ValueConverter<T, String> createStringValueConverter(Class<T> valueClass) {
Expand All @@ -113,8 +145,12 @@ protected <T, DBT> PrototypedEntityField<E, T> prototypedField(EntityFieldProtot
}

protected <T, DBT> PrototypedEntityField<E, T> prototypedField(EntityFieldPrototype<T> entityFieldPrototype, TableField<Record, DBT> tableField, ValueConverter<T, DBT> valueConverter, EntityValueEqualityFunction<T> valueEqualityFunction) {
PrototypedEntityFieldImpl<E, T> field = new PrototypedEntityFieldImpl<>(this, entityFieldPrototype, new SimpleEntityFieldDbAdapter<>(tableField, valueConverter),
createStringValueConverter(valueConverter.getValueClass()), valueEqualityFunction);
final var field = PrototypedEntityFieldImpl.<E, T>builder(this)
.withDbAdapter(new SimpleEntityFieldDbAdapter<>(tableField, valueConverter))
.withStringValueConverter(createStringValueConverter(valueConverter.getValueClass()))
.withValueEqualityFunction(valueEqualityFunction)
.withEntityFieldPrototype(entityFieldPrototype)
.build();
prototypedFields.add(field);
return addField(field);
}
Expand Down
19 changes: 19 additions & 0 deletions main/src/main/java/com/kenshoo/pl/entity/EntityField.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import java.util.Arrays;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public interface EntityField<E extends EntityType<E>, T> {
Expand All @@ -16,6 +17,8 @@ public interface EntityField<E extends EntityType<E>, T> {

boolean valuesEqual(T v1, T v2);

int compareValues(T v1, T v2);

default Class<T> getValueClass() {
return getStringValueConverter().getValueClass();
}
Expand Down Expand Up @@ -78,4 +81,20 @@ default PLCondition isNotNull() {
final TableField<Record, Object> tableField = (TableField<Record, Object>)getDbAdapter().getFirstTableField();
return new PLCondition(tableField.isNotNull(), entity -> entity.safeGet(this).isNotNull(), this);
}

default PLCondition greaterThan(T value) {
if (isVirtual()) {
throw new UnsupportedOperationException("The greaterThan operation is unsupported for virtual fields");
}

final Object tableValue = getDbAdapter().getFirstDbValue(value);
@SuppressWarnings("unchecked") final var tableField = (TableField<Record, Object>) getDbAdapter().getFirstTableField();
return new PLCondition(tableField.greaterThan(tableValue), entityFieldGreaterThan(value), this);
}

private Predicate<Entity> entityFieldGreaterThan(T value) {
return entity -> entity.safeGet(this)
.filter(entityValue -> compareValues(entityValue, value) > 0)
.isPresent();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.kenshoo.pl.entity;

import java.util.Comparator;

public interface MutableEntityField<E extends EntityType<E>, T> extends EntityField<E, T> {

EntityField<E, T> comparedBy(final Comparator<T> valueComparator);
}
Copy link
Contributor Author

@effiban effiban Feb 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interface is needed for the DSL to work properly - after a field has been created with field() we need to be able to optionally add a comparator. On the other hand we don't want this to be part of the regular EntityField interface used in many places that should be effectively 'immutable'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@effiban
Let's call this interface ComparableEntityField

Copy link
Contributor Author

@effiban effiban Feb 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that it is only comparable after you call the method. If you use this name, then every field we already have today will implement this interface, which might be confusing since some of them really are not comparable.
Need to think of something similar that means "optionally comparable"

Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package com.kenshoo.pl.entity.internal;

import com.kenshoo.pl.entity.*;
import com.kenshoo.pl.entity.equalityfunctions.EntityValueEqualityFunction;

import java.util.Comparator;
import java.util.Optional;

import static java.util.Objects.requireNonNull;

abstract class AbstractEntityField<
E extends EntityType<E>,
T,
F extends EntityField<E, T>,
B extends AbstractEntityField.Builder<E, T, F, B>> implements MutableEntityField<E, T> {
Copy link
Contributor Author

@effiban effiban Feb 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make the builder inheritance work smoothly without casting, I needed to do the following:

  • Add extra generic params for the field and builder types
  • Add this abstract base class on top of all existing ones (it doesn't work well when the base class is the concrete EntityFieldImpl)


protected final EntityType<E> entityType;

protected final EntityFieldDbAdapter<T> dbAdapter;

protected final EntityValueEqualityFunction<T> valueEqualityFunction;

protected final Comparator<T> valueComparator;

protected final ValueConverter<T, String> stringValueConverter;

protected AbstractEntityField(final EntityType<E> entityType,
final EntityFieldDbAdapter<T> dbAdapter,
final ValueConverter<T, String> stringValueConverter,
final EntityValueEqualityFunction<T> valueEqualityFunction,
final Comparator<T> valueComparator) {
this.entityType = entityType;
this.dbAdapter = dbAdapter;
this.stringValueConverter = stringValueConverter;
this.valueEqualityFunction = valueEqualityFunction;
this.valueComparator = valueComparator;
}

@Override
public EntityFieldDbAdapter<T> getDbAdapter() {
return dbAdapter;
}

@Override
public ValueConverter<T, String> getStringValueConverter() {
return stringValueConverter;
}

@Override
public boolean valuesEqual(T v1, T v2) {
return valueEqualityFunction.apply(v1, v2);
}

@Override
public int compareValues(final T v1, final T v2) {
return comparatorOf(v1).compare(v1, v2);
}

@Override
public EntityType<E> getEntityType() {
return entityType;
}

protected B toBuilder() {
return newBuilder(entityType)
.withDbAdapter(dbAdapter)
.withValueEqualityFunction(valueEqualityFunction)
.withValueComparator(valueComparator)
.withStringValueConverter(stringValueConverter);
}

protected abstract B newBuilder(final EntityType<E> entityType);

@Override
public EntityField<E, T> comparedBy(final Comparator<T> valueComparator) {
requireNonNull(valueComparator);
return toBuilder()
.withValueComparator(valueComparator)
.withValueEqualityFunction(equalityFunctionOf(valueComparator))
.build();
}

@Override
public String toString() {
return entityType.toFieldName(this);
}

private Comparator<T> comparatorOf(T value) {
return Optional.ofNullable(valueComparator)
.orElseGet(() -> {
if (value instanceof Comparable<?>) {
//noinspection unchecked
return (Comparator<T>) Comparator.naturalOrder();
}
throw new UnsupportedOperationException(String.format("The field [%s] is not comparable for the value [%s]", this, value));
});
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation is designed to add support for comparison operations for all fields as much as possible.

  • If the field is comparable:
    • It will use the natural comparator by default.
    • If the user overrode this, it will use the specific one
  • If the field is not comparable - an exception will be thrown



private EntityValueEqualityFunction<T> equalityFunctionOf(final Comparator<T> valueComparator) {
return (v1, v2) -> valueComparator.compare(v1, v2) == 0;
}

protected abstract static class Builder<
E extends EntityType<E>,
T,
F extends EntityField<E, T>,
B extends Builder<E, T, F, B>> {

protected final EntityType<E> entityType;

protected EntityFieldDbAdapter<T> dbAdapter;

protected EntityValueEqualityFunction<T> valueEqualityFunction;

protected Comparator<T> valueComparator;

protected ValueConverter<T, String> stringValueConverter;

protected Builder(final EntityType<E> entityType) {
this.entityType = entityType;
}

public B withDbAdapter(final EntityFieldDbAdapter<T> dbAdapter) {
this.dbAdapter = dbAdapter;
return self();
}

public B withValueEqualityFunction(final EntityValueEqualityFunction<T> valueEqualityFunction) {
this.valueEqualityFunction = valueEqualityFunction;
return self();
}

public B withValueComparator(final Comparator<T> valueComparator) {
this.valueComparator = valueComparator;
return self();
}

public B withStringValueConverter(final ValueConverter<T, String> stringValueConverter) {
this.stringValueConverter = stringValueConverter;
return self();
}

protected abstract B self();

public abstract F build();
}
}
Loading