Skip to content
New issue

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

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

Already on GitHub? # to your account

feat: annotation support for getters #792

Open
wants to merge 4 commits into
base: v3
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ private static class Holder {
.disable(
DeserializationFeature
.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) // Nano seconds not supported by the

.disable(
SerializationFeature
.FAIL_ON_EMPTY_BEANS)
// engine
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ public CompletableFuture<UpdateObjectResponse> partialUpdateObjectAsync(
Objects.requireNonNull(data, "Data is required.");
Objects.requireNonNull(createIfNotExists, "createIfNotExists is required.");

String objectID = AlgoliaUtils.getObjectID(data, clazz);
String objectID = AlgoliaUtils.getObjectID(data);

if (requestOptions == null) {
requestOptions = new RequestOptions();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,9 +330,9 @@ public SearchResult<T> setFacets_stats(Map<String, FacetStats> facets_stats) {
return this;
}

public int getObjectPosition(@Nonnull String objectID, @Nonnull Class<T> clazz) {
public int getObjectPosition(@Nonnull String objectID) {
return IntStream.range(0, hits.size())
.filter(i -> objectID.equals(AlgoliaUtils.getObjectID(hits.get(i), clazz)))
.filter(i -> objectID.equals(AlgoliaUtils.getObjectID(hits.get(i))))
.findFirst()
.orElse(-1);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,162 +1,100 @@
package com.algolia.search.util;

import com.algolia.search.Defaults;
import com.algolia.search.exceptions.AlgoliaRuntimeException;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.lang.reflect.Field;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;

import java.time.Clock;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.zone.ZoneRules;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.*;
import javax.annotation.Nonnull;

public class AlgoliaUtils {

/** Checks if the given string is empty or white spaces */
public static Boolean isEmptyWhiteSpace(final String stringToCheck) {
return stringToCheck.trim().length() == 0;
}

/** Checks if the given string is null, empty or white spaces */
public static Boolean isNullOrEmptyWhiteSpace(final String stringToCheck) {
return stringToCheck == null || stringToCheck.trim().length() == 0;
}

private static final ZoneRules ZONE_RULES_UTC = ZoneOffset.UTC.getRules();

/**
* Memory optimization for getZoneRules with the same ZoneOffset (UTC). ZoneRules is immutable and
* threadsafe, but getRules method consumes a lot of memory during load testing.
*/
public static OffsetDateTime nowUTC() {
final Instant now = Clock.system(ZoneOffset.UTC).instant();
return OffsetDateTime.ofInstant(now, ZONE_RULES_UTC.getOffset(now));
}
public static final String PROPERTY_OBJECT_ID = "objectID";

/**
* Ensure that the objectID field or the @JsonProperty(\"objectID\")" is present in the given
* class
*
* @param clazz The class to scan
* @throws AlgoliaRuntimeException When the class doesn't have an objectID field or a Jackson
* annotation @JsonProperty(\"objectID\"")
*/
public static <T> void ensureObjectID(@Nonnull Class<T> clazz) {
// Try to find the objectID field
Field objectIDField = getField(clazz, "objectID");

// If objectID field doesn't exist, let's check for Jackson annotations in all the fields
Optional<Field> optObjectIDField = findObjectIDInAnnotation(clazz);

if (objectIDField == null && !optObjectIDField.isPresent()) {
throw new AlgoliaRuntimeException(
"The "
+ clazz
+ " must have an objectID property or a Jackson annotation @JsonProperty(\"objectID\")");
/**
* Checks if the given string is empty or white spaces
*/
public static Boolean isEmptyWhiteSpace(final String stringToCheck) {
return stringToCheck.trim().isEmpty();
}
}

/**
* Get the objectID of the given class at runtime
*
* @param clazz The class to scan
* @throws AlgoliaRuntimeException When the class doesn't have an objectID field or a Jackson
* annotation @JsonProperty(\"objectID\"")
*/
public static <T> String getObjectID(@Nonnull T data, @Nonnull Class<T> clazz) {
/**
* Checks if the given string is null, empty or white spaces
*/
public static Boolean isNullOrEmptyWhiteSpace(final String stringToCheck) {
return stringToCheck == null || stringToCheck.trim().isEmpty();
}

String objectID = null;
private static final ZoneRules ZONE_RULES_UTC = ZoneOffset.UTC.getRules();

// Try to find the objectID field
try {
Field objectIDField = getField(clazz, "objectID");
if (objectIDField != null) {
objectID = (String) objectIDField.get(data);
}
} catch (
IllegalAccessException
ignored) { // Ignored because if it fails we want to move forward on annotations
/**
* Memory optimization for getZoneRules with the same ZoneOffset (UTC). ZoneRules is immutable and
* thread-safe, but getRules method consumes a lot of memory during load testing.
*/
public static OffsetDateTime nowUTC() {
final Instant now = Clock.system(ZoneOffset.UTC).instant();
return OffsetDateTime.ofInstant(now, ZONE_RULES_UTC.getOffset(now));
}

if (objectID != null) {
return objectID;
/**
* Ensure that the objectID field or the @JsonProperty(\"objectID\")" is present in the given
* class
*
* @param clazz The class to scan
* @throws AlgoliaRuntimeException When the class doesn't have an objectID field or a Jackson
* annotation @JsonProperty(\"objectID\"")
*/
public static <T> void ensureObjectID(@Nonnull Class<T> clazz) {
BeanDescription introspection = introspectClass(clazz);
if (!containsObjectID(introspection)) throw objectIDNotFoundException(clazz);
}

// If objectID field doesn't exist, let's check for Jackson annotations in all the fields
Optional<Field> optObjectIDField = findObjectIDInAnnotation(clazz);

if (optObjectIDField.isPresent()) {
Field objectIDField = optObjectIDField.get();
try {
objectIDField.setAccessible(true);

objectID = (String) objectIDField.get(data);

if (objectID != null) {
return objectID;
}

} catch (IllegalAccessException ignored) {
throw new AlgoliaRuntimeException("Can't access the ObjectID field.");
}
private static <T> AlgoliaRuntimeException objectIDNotFoundException(Class<T> clazz) {
return new AlgoliaRuntimeException(
"The " + clazz + " must have an objectID property or a Jackson annotation @JsonProperty(\"objectID\")");
}

// If non of the both above the method fails
throw new AlgoliaRuntimeException(
"The "
+ clazz
+ " must have an objectID property or a Jackson annotation @JsonProperty(\"objectID\")");
}

private static Optional<Field> findObjectIDInAnnotation(@Nonnull Class<?> clazz) {
List<Field> fields = getFields(clazz);
return fields.stream()
.filter(
f ->
f.getAnnotation(JsonProperty.class) != null
&& f.getAnnotation(JsonProperty.class).value().equals("objectID"))
.findFirst();
}

/**
* Recursively search for the given field in the given class
*
* @param clazz The class to reflect on
* @param fieldName The field to reach
*/
private static Field getField(@Nonnull Class<?> clazz, @Nonnull String fieldName) {
Class<?> tmpClass = clazz;
do {
try {
Field f = tmpClass.getDeclaredField(fieldName);
f.setAccessible(true);
return f;
} catch (NoSuchFieldException e) {
tmpClass = tmpClass.getSuperclass();
}
} while (tmpClass != null);
/**
* Checks if the {@value PROPERTY_OBJECT_ID} is present in the classes public fields, getter methods or
* annotations using Jackson's {@link BeanDescription}
*/
protected static boolean containsObjectID(BeanDescription introspection) {
return introspection.findProperties().stream()
.filter(d -> d.getPrimaryType().isTypeOrSubTypeOf(String.class))
.anyMatch(d -> PROPERTY_OBJECT_ID.equals(d.getName()));
}

return null;
}
/**
* Introspection of the class using Jackson
*/
protected static <T> BeanDescription introspectClass(Class<T> clazz) {
ObjectMapper mapper = getMapper();
JavaType type = mapper.getTypeFactory().constructType(clazz);
return mapper.getSerializationConfig().introspect(type);
}

/**
* Recursively search for all fields in the given class
*
* @param clazz The class to reflect on
*/
private static List<Field> getFields(@Nonnull Class<?> clazz) {
List<Field> result = new ArrayList<>();
Class<?> i = clazz;
private static ObjectMapper getMapper() {
return Defaults.getObjectMapper();
}

while (i != null && i != Object.class) {
Collections.addAll(result, i.getDeclaredFields());
i = i.getSuperclass();
/**
* Get the objectID of the given class at runtime
*
* @throws AlgoliaRuntimeException When the class doesn't have an objectID field or a Jackson
* annotation @JsonProperty(\"objectID\"")
*/
public static <T> String getObjectID(@Nonnull T data) {
return Optional.ofNullable(getMapper().valueToTree(data)
.get(PROPERTY_OBJECT_ID))
.filter(JsonNode::isTextual)
.map(JsonNode::asText)
.orElseThrow(() -> objectIDNotFoundException(data.getClass()));
}

return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ void testGetObjectIDWithoutObjectID() {
assertThatThrownBy(
() ->
AlgoliaUtils.getObjectID(
new DummyObjectWithoutObjectId(), DummyObjectWithoutObjectId.class))
new DummyObjectWithoutObjectId()))
.isInstanceOf(AlgoliaRuntimeException.class)
.hasMessageContaining(
"must have an objectID property or a Jackson annotation @JsonProperty(\"objectID\")");

assertThatThrownBy(
() ->
AlgoliaUtils.getObjectID(
new DummyChildWithoutObjectID(), DummyChildWithoutObjectID.class))
new DummyChildWithoutObjectID()))
.isInstanceOf(AlgoliaRuntimeException.class)
.hasMessageContaining(
"must have an objectID property or a Jackson annotation @JsonProperty(\"objectID\")");
Expand All @@ -85,15 +85,15 @@ void testGetObjectIDWithWrongAnnotation() {
assertThatThrownBy(
() ->
AlgoliaUtils.getObjectID(
new DummyObjectWithWrongAnnotation(), DummyObjectWithWrongAnnotation.class))
new DummyObjectWithWrongAnnotation()))
.isInstanceOf(AlgoliaRuntimeException.class)
.hasMessageContaining(
"must have an objectID property or a Jackson annotation @JsonProperty(\"objectID\")");

assertThatThrownBy(
() ->
AlgoliaUtils.getObjectID(
new DummyChildWithWrongAnnotation(), DummyChildWithWrongAnnotation.class))
new DummyChildWithWrongAnnotation()))
.isInstanceOf(AlgoliaRuntimeException.class)
.hasMessageContaining(
"must have an objectID property or a Jackson annotation @JsonProperty(\"objectID\")");
Expand All @@ -105,15 +105,15 @@ void testGetObjectIDWithObjectID() {
assertThatCode(
() ->
AlgoliaUtils.getObjectID(
new DummyObjectWithObjectID().setObjectID("foo"),
DummyObjectWithObjectID.class))
new DummyObjectWithObjectID().setObjectID("foo")
))
.doesNotThrowAnyException();

assertThatCode(
() ->
AlgoliaUtils.getObjectID(
(DummyChildWithObjectID) new DummyChildWithObjectID().setObjectID("foo"),
DummyChildWithObjectID.class))
(DummyChildWithObjectID) new DummyChildWithObjectID().setObjectID("foo")
))
.doesNotThrowAnyException();
}

Expand All @@ -123,14 +123,14 @@ void testGetObjectIDWithAnnotation() {
assertThatCode(
() ->
AlgoliaUtils.getObjectID(
new DummyObjectWithAnnotation().setId("foo"), DummyObjectWithAnnotation.class))
new DummyObjectWithAnnotation().setId("foo")))
.doesNotThrowAnyException();

assertThatCode(
() ->
AlgoliaUtils.getObjectID(
(DummyChildWithAnnotation) new DummyChildWithAnnotation().setId("foo"),
DummyChildWithAnnotation.class))
(DummyChildWithAnnotation) new DummyChildWithAnnotation().setId("foo")
))
.doesNotThrowAnyException();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ void testSearch() throws ExecutionException, InterruptedException {
searchFacetFuture);

assertThat(searchAlgoliaFuture.get().getHits()).hasSize(2);
assertThat(searchAlgoliaFuture.get().getObjectPosition("nicolas-dessaigne", Employee.class))
assertThat(searchAlgoliaFuture.get().getObjectPosition("nicolas-dessaigne"))
.isEqualTo(0);
assertThat(searchAlgoliaFuture.get().getObjectPosition("julien-lemoine", Employee.class))
assertThat(searchAlgoliaFuture.get().getObjectPosition("julien-lemoine"))
.isEqualTo(1);
assertThat(searchAlgoliaFuture.get().getObjectPosition("unknown", Employee.class))
assertThat(searchAlgoliaFuture.get().getObjectPosition("unknown"))
.isEqualTo(-1);
assertTrue(searchAlgoliaFuture.get().getExhaustiveNbHits());
assertThat(searchElonFuture.get().getQueryID()).isNotNull();
Expand Down
Loading