Skip to content

Commit

Permalink
Implemented local id (lid) support (#270)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasminb authored Jul 31, 2024
1 parent 808c66d commit 555f6dc
Show file tree
Hide file tree
Showing 12 changed files with 364 additions and 38 deletions.
62 changes: 60 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,10 @@ Note that `@Type` annotation is not inherited from supperclasses.

Id annotation is used to flag an attribute of a class as an `id` attribute. Each resource class must have an id field.

In case field annotated by the `@Id` annotation is not a `String` field, `@Id` annotation needs to be configured with proper `ResourceIdHandler`. Lirary provides handlers for `Long` and `Integer` types, in case types other than those mentioned are used, user must implement and provide proper id handler.
In case field annotated by the `@Id` annotation is not a `String` field, `@Id` annotation needs to be configured with proper `ResourceIdHandler`.
Library provides handlers for `Long` and `Integer` types, in case types other than those mentioned are used, user must implement and provide proper id handler.

Id is a special attribute that is, together with type, used to uniquely identify an resource.
Id is a special attribute that is, together with type, used to uniquely identify a resource.

Id annotation is inheritable, one can define a base model class that contains a field with `@Id` annotation and then extend it to create a new type.

Expand Down Expand Up @@ -133,6 +134,63 @@ public class Book {

```

#### LocalId annotation

This annotation is used to mark a class attribute as a holder of the `local identifier`.
Local identifiers may be used in cases where requests are originating on the client side. This annotation is optional
and behaves in a similar way to the `@Id` annotation. It must be represented as a string in serialized form
which means that for non-string class attributes, handler must be registered to convert from and to string type on
serialization/deserialization.

Example:

```java
@Type("book")
public class Book {

@Id
private String isbn;
@LocalId
private String localId;
}
```

By default, library will not serialize local id attribute, its serialization needs to be enabled explicitly:

```java
SerializationSettings settings = new SerializationSettings.Builder()
.serializeLocalId(true)
.build();

// Using the settings on serialization call
converter.writeDocument(document, settings);

```

It can also be enabled globally using the `SerializationFeature` options:

```java
converter.enableSerializationOption(SerializationFeature.INCLUDE_LOCAL_ID);
```

For deserialization, `lid` attribute is optional by default, it can be made required by `DeserializationFeature`
mechanism:

```java
converter.enableDeserializationOption(DeserializationFeature.REQUIRE_LOCAL_RESOURCE_ID);
```

Setting the option above means that deserialization will throw in case resource does not contain non-empty `lid` attribute.

###### Important note

By default `REQUIRE_RESOURCE_ID` deserialization feature is enabled, which means that for server-side usage,
where users want to use the `lid` mechanism, this option should be disabled:

```java
converter.disableDeserializationOption(DeserializationFeature.REQUIRE_RESOURCE_ID);
```

#### Relationship annotation

Relationship annotation is used to designate other resource types as a relationships.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.jasminb.jsonapi;

import com.github.jasminb.jsonapi.annotations.Id;
import com.github.jasminb.jsonapi.annotations.LocalId;
import com.github.jasminb.jsonapi.annotations.Meta;
import com.github.jasminb.jsonapi.annotations.Relationship;
import com.github.jasminb.jsonapi.annotations.RelationshipLinks;
Expand All @@ -26,7 +27,10 @@ public class ConverterConfiguration {
private final Map<String, Class<?>> typeToClassMapping = new HashMap<>();
private final Map<Class<?>, Type> typeAnnotations = new HashMap<>();
private final Map<Class<?>, Field> idMap = new HashMap<>();
private final Map<Class<?>, Field> localIdMap = new HashMap<>();
private final Map<Class<?>, ResourceIdHandler> idHandlerMap = new HashMap<>();
private final Map<Class<?>, ResourceIdHandler> localIdHandlerMap = new HashMap<>();

private final Map<Class<?>, List<Field>> relationshipMap = new HashMap<>();
private final Map<Class<?>, Map<String, Class<?>>> relationshipTypeMap = new HashMap<>();
private final Map<Class<?>, Map<String, Field>> relationshipFieldMap = new HashMap<>();
Expand Down Expand Up @@ -84,26 +88,26 @@ private void processClass(Class<?> clazz) {
}

relationshipMap.put(clazz, relationshipFields);

// collecting RelationshipMeta fields
List<Field> relMetaFields = ReflectionUtils.getAnnotatedFields(clazz, RelationshipMeta.class, true);

for (Field relMetaField : relMetaFields) {
relMetaField.setAccessible(true);

RelationshipMeta relationshipMeta = relMetaField.getAnnotation(RelationshipMeta.class);
Class<?> targetType = ReflectionUtils.getFieldType(relMetaField);
relationshipMetaTypeMap.get(clazz).put(relationshipMeta.value(), targetType);
fieldRelationshipMetaMap.put(relMetaField, relationshipMeta);
relationshipMetaFieldMap.get(clazz).put(relationshipMeta.value(), relMetaField);
}

// Collecting RelationshipLink fields
List<Field> relLinkFields = ReflectionUtils.getAnnotatedFields(clazz, RelationshipLinks.class, true);

for (Field relLinkField : relLinkFields) {
relLinkField.setAccessible(true);

RelationshipLinks links = relLinkField.getAnnotation(RelationshipLinks.class);
relationshipLinksFieldMap.get(clazz).put(links.value(), relLinkField);
}
Expand All @@ -127,7 +131,22 @@ private void processClass(Class<?> clazz) {
} else {
throw new IllegalArgumentException("Only single @Id annotation is allowed per defined type!");
}
}

// Collecting local id fields
List<Field> localIdFields = ReflectionUtils.getAnnotatedFields(clazz, LocalId.class, true);

if (localIdFields.size() == 1) {
Field localIdField = localIdFields.get(0);
localIdField.setAccessible(true);
localIdMap.put(clazz, localIdField);
try {
localIdHandlerMap.put(clazz, localIdField.getAnnotation(LocalId.class).value().newInstance());
} catch (InstantiationException | IllegalAccessException e) {
throw new IllegalArgumentException("Unable to construct handler instance by using no-arg constructor", e);
}
} else if (localIdFields.size() > 1) {
throw new IllegalArgumentException("Only single @LocalId annotation is allowed per defined type!");
}

// Collecting Meta fields
Expand Down Expand Up @@ -215,7 +234,16 @@ public Class<?> getTypeClass(String typeName) {
public Field getIdField(Class<?> clazz) {
return idMap.get(clazz);
}


/**
* Returns the id field for given type.
* @param clazz {@link Class} type to resolve id field for
* @return {@link Field} id field
*/
public Field getLocalIdField(Class<?> clazz) {
return localIdMap.get(clazz);
}

/**
* Returns handler registered for given type's id field.
*
Expand All @@ -226,6 +254,16 @@ public ResourceIdHandler getIdHandler(Class<?> clazz) {
return idHandlerMap.get(clazz);
}

/**
* Returns handler registered for given type's local id field.
*
* @param clazz {@link Class} type to resolve local id handler for
* @return handler
*/
public ResourceIdHandler getLocalIdHandler(Class<?> clazz) {
return localIdHandlerMap.get(clazz);
}

/**
* Returns relationship field.
* @param clazz {@link Class} class holding relationship
Expand Down Expand Up @@ -290,7 +328,7 @@ public String getTypeName(Class<?> clazz) {
}
return null;
}

/**
* Resolves and returns the type given to provided class.
* @param clazz {@link Class} to resolve type name for
Expand Down Expand Up @@ -324,7 +362,7 @@ public static boolean isEligibleType(Class<?> type) {
return type.isAnnotationPresent(Type.class) &&
!ReflectionUtils.getAnnotatedFields(type, Id.class, true).isEmpty();
}

/**
* Returns relationship meta field.
* @param clazz {@link Class} class holding relationship
Expand All @@ -334,7 +372,7 @@ public static boolean isEligibleType(Class<?> type) {
public Field getRelationshipMetaField(Class<?> clazz, String relationshipName) {
return relationshipMetaFieldMap.get(clazz).get(relationshipName);
}

/**
* Returns a type of a relationship meta field.
* @param clazz {@link Class} owning the field with relationship meta annotation
Expand All @@ -344,7 +382,7 @@ public Field getRelationshipMetaField(Class<?> clazz, String relationshipName) {
public Class<?> getRelationshipMetaType(Class<?> clazz, String relationshipName) {
return relationshipMetaTypeMap.get(clazz).get(relationshipName);
}

/**
* Returns relationship links field.
* @param clazz {@link Class} class holding relationship
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ public enum DeserializationFeature {
*/
REQUIRE_RESOURCE_ID(true),

/**
* This option enforces presence of the 'lid' attribute in resources being parsed.
*/
REQUIRE_LOCAL_RESOURCE_ID(false),

/**
* This option determines whether encountering unknown types results in {@link IllegalArgumentException} being
* thrown, or if parsing continues and the unknown field is ignored.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public interface JSONAPISpecConstants {
String ATTRIBUTES = "attributes";
String TYPE = "type";
String ID = "id";
String LOCAL_ID = "lid";
String RELATIONSHIPS = "relationships";
String INCLUDED = "included";
String LINKS = "links";
Expand Down
Loading

0 comments on commit 555f6dc

Please sign in to comment.