Skip to content

Commit

Permalink
Added @FieldOrder for bean fields to define their declaration order m…
Browse files Browse the repository at this point in the history
…anually (since some JVM implementations do not provide them in the right order naturally)
  • Loading branch information
SimonDan committed Jun 11, 2020
1 parent 47b5f49 commit 573f71a
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package de.adito.ojcms.beans.annotations;

import java.lang.annotation.*;

/**
* Use this annotation for bean fields to define their declaration order. This is necessary if you are using a JVM implementation that
* does not provide fields in their declaration order naturally (via {@link Class#getDeclaredFields()}.
* <p>
* If this annotation is used at least for one bean field, all other fields must be annotated as well. Otherwise a runtime exception will
* be thrown. The order numbers for a bean class must not contain duplicates, but may have gaps (like 100, 200, 300, ...).
* <p>
* Lower numbers are declared before higher numbers.
*
* @author Simon Danner, 11.06.2020
*/
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldOrder
{
/**
* A number indicating the declaration position of the annotated bean field (lower number = lower position)
*/
int value();
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public BeanFieldDuplicateException(List<IField<?>> pDuplicateFields)
{
super("A bean cannot define a field twice! duplicates: " + pDuplicateFields.stream() //
.map(IField::getName) //
.collect(Collectors.joining(", ")));
.collect(Collectors.joining(", ")) + "\n Take a look at @FieldOrder as well!");
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package de.adito.ojcms.beans.util;

import de.adito.ojcms.beans.*;
import de.adito.ojcms.beans.annotations.FieldOrder;
import de.adito.ojcms.beans.exceptions.OJInternalException;
import de.adito.ojcms.beans.exceptions.bean.NoDeclaredBeanTypeException;
import de.adito.ojcms.beans.exceptions.field.BeanFieldCreationException;
import de.adito.ojcms.beans.literals.fields.IField;

import java.lang.annotation.Annotation;
Expand Down Expand Up @@ -87,7 +89,7 @@ public static Class<? extends IBean> requiresDeclaredBeanType(Class<? extends IB
* @param <ANNOTATION> the generic type of the annotation
*/
public static <ANNOTATION extends Annotation> void doIfAnnotationPresent(Class<?> pType, Class<ANNOTATION> pAnnotationType,
Consumer<ANNOTATION> pAction)
Consumer<ANNOTATION> pAction)
{
if (pType.isAnnotationPresent(pAnnotationType))
pAction.accept(pType.getAnnotation(pAnnotationType));
Expand Down Expand Up @@ -117,16 +119,47 @@ private static List<IField<?>> _createBeanMetadata(Class<? extends IBean> pBeanT
}

/**
* Returns all public and static bean fields from a bean class type.
* Returns all public, static and final bean fields from a bean class type. This method must return the fields in their textual
* declaration order. If the JVM implementation won't do that naturally, use {@link FieldOrder}.
*
* @param pBeanType the type of the bean, which must be a sub class of {@link OJBean} to own specific fields
* @return a list of declared fields of the bean type
*/
private static List<Field> _getDeclaredBeanFields(Class<? extends IBean> pBeanType)
{
return _getDeclaredFields(pBeanType, //
final List<Field> unsortedDeclaredFields = _getDeclaredFields(pBeanType, //
pField -> Modifier.isStatic(pField.getModifiers()), //
pField -> IField.class.isAssignableFrom(pField.getType()));

final Map<Field, Integer> fieldOrderByField = new HashMap<>();

for (Field field : unsortedDeclaredFields)
{
if (!Modifier.isFinal(field.getModifiers()))
throw new BeanFieldCreationException(
"Bean field is not declared as final! bean-type: " + pBeanType.getName() + " field name: " + field.getName());

if (field.isAnnotationPresent(FieldOrder.class))
fieldOrderByField.put(field, field.getAnnotation(FieldOrder.class).value());
}

if (fieldOrderByField.isEmpty())
return unsortedDeclaredFields; //They should be sorted naturally by JVM implementation

//Does every field have a field order annotation?
if (fieldOrderByField.size() != unsortedDeclaredFields.size())
throw new BeanFieldCreationException(
"If @FieldOrder is used, every field of the bean type must be annotated! bean-type: " + pBeanType.getName());

//Check for duplicates in order numbers
final Set<Integer> uniqueOrderNumbers = new HashSet<>(fieldOrderByField.values());
if (uniqueOrderNumbers.size() != fieldOrderByField.size())
throw new BeanFieldCreationException("All order numbers of @FieldOrders must be unique!");

return fieldOrderByField.entrySet().stream() //
.sorted(Map.Entry.comparingByValue()) //
.map(Map.Entry::getKey) //
.collect(Collectors.toList());
}

/**
Expand Down
71 changes: 70 additions & 1 deletion ojcms-beans/src/test/java/de/adito/ojcms/beans/BeanTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import de.adito.ojcms.beans.base.IEqualsHashCodeChecker;
import de.adito.ojcms.beans.datasource.IBeanDataSource;
import de.adito.ojcms.beans.exceptions.bean.*;
import de.adito.ojcms.beans.exceptions.field.BeanFieldDoesNotExistException;
import de.adito.ojcms.beans.exceptions.field.*;
import de.adito.ojcms.beans.literals.fields.IField;
import de.adito.ojcms.beans.literals.fields.types.*;
import de.adito.ojcms.beans.literals.fields.util.FieldValueTuple;
Expand Down Expand Up @@ -210,6 +210,30 @@ public void testBeanInheritance()
assertEquals(42, bean.getValue(ConcreteBeanType.SOME_SPECIAL_FIELD));
}

@Test
public void testBeanFieldOrderDeclaration()
{
new FieldOrderedBean();
}

@Test
public void testBeanFieldOrderDeclaration_Illegal()
{
assertThrows(BeanFieldDuplicateException.class, IllegalOrderedBean::new);
}

@Test
public void testBeanFieldOrderDeclaration_MissingAnnotation()
{
assertThrows(ExceptionInInitializerError.class, MissingOrderAnnotationBean::new);
}

@Test
public void testBeanFieldOrderDeclaration_Duplicate()
{
assertThrows(ExceptionInInitializerError.class, DuplicateOrderAnnotationBean::new);
}

/**
* Creates a new bean text field.
*
Expand Down Expand Up @@ -340,6 +364,51 @@ public static class ConcreteBeanType extends AbstractBaseBeanType
public static final IntegerField SOME_SPECIAL_FIELD = OJFields.create(ConcreteBeanType.class);
}

/**
* A bean defining its fields' declaration order.
*/
public static class FieldOrderedBean extends OJBean
{
@FieldOrder(0)
public static final IntegerField FIRST_FIELD = OJFields.create(FieldOrderedBean.class);
@FieldOrder(1)
public static final IntegerField SECOND_FIELD = OJFields.create(FieldOrderedBean.class);
@FieldOrder(4) //Gaps should be allowed
public static final IntegerField THIRD_FIELD = OJFields.create(FieldOrderedBean.class);
}

/**
* A bean defining a wrong/illegal declaration order.
*/
public static class IllegalOrderedBean extends OJBean
{
@FieldOrder(1)
public static final IntegerField FIRST_FIELD = OJFields.create(IllegalOrderedBean.class);
@FieldOrder(0)
public static final IntegerField SECOND_FIELD = OJFields.create(IllegalOrderedBean.class);
}

/**
* A bean missing a field order annotation.
*/
public static class MissingOrderAnnotationBean extends OJBean
{
@FieldOrder(0)
public static final IntegerField FIRST_FIELD = OJFields.create(MissingOrderAnnotationBean.class);
public static final IntegerField SECOND_FIELD = OJFields.create(MissingOrderAnnotationBean.class);
}

/**
* A bean defining a duplicate field order annotation.
*/
public static class DuplicateOrderAnnotationBean extends OJBean
{
@FieldOrder(0)
public static final IntegerField FIRST_FIELD = OJFields.create(DuplicateOrderAnnotationBean.class);
@FieldOrder(0)
public static final IntegerField SECOND_FIELD = OJFields.create(DuplicateOrderAnnotationBean.class);
}

/**
* A bean data source that always returns null values to test {@link NeverNull} fields.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ public void testBaseContainerWithPresetContent()
public static abstract class SomeBaseBean extends OJBean
{
@Identifier
public static IntegerField FIELD1 = OJFields.create(SomeBaseBean.class);
public static final IntegerField FIELD1 = OJFields.create(SomeBaseBean.class);
@Identifier
public static LongField FIELD2 = OJFields.create(SomeBaseBean.class);
public static BooleanField FIELD3 = OJFields.create(SomeBaseBean.class);
public static final LongField FIELD2 = OJFields.create(SomeBaseBean.class);
public static final BooleanField FIELD3 = OJFields.create(SomeBaseBean.class);

SomeBaseBean(int pValue1, long pValue2, boolean pValue3)
{
Expand All @@ -85,7 +85,7 @@ private SomeBaseBean()

public static class SomeConcreteBean extends SomeBaseBean
{
public static TextField FIELD4 = OJFields.create(SomeConcreteBean.class);
public static final TextField FIELD4 = OJFields.create(SomeConcreteBean.class);

SomeConcreteBean(int pValue1, long pValue2, boolean pValue3, String pValue4)
{
Expand All @@ -101,8 +101,8 @@ private SomeConcreteBean()

public static class SomeOtherConcreteBean extends SomeBaseBean
{
public static TextField FIELD5 = OJFields.create(SomeOtherConcreteBean.class);
public static TimestampField FIELD6 = OJFields.create(SomeOtherConcreteBean.class);
public static final TextField FIELD5 = OJFields.create(SomeOtherConcreteBean.class);
public static final TimestampField FIELD6 = OJFields.create(SomeOtherConcreteBean.class);

SomeOtherConcreteBean(int pValue1, long pValue2, boolean pValue3, String pValue5)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package de.adito.ojcms.rest.auth.api;

import de.adito.ojcms.beans.*;
import de.adito.ojcms.beans.annotations.FinalNeverNull;
import de.adito.ojcms.beans.annotations.*;
import de.adito.ojcms.beans.literals.fields.types.TextField;

/**
Expand All @@ -12,8 +12,11 @@
public class AuthenticationRequest extends OJBean
{
@FinalNeverNull
@FieldOrder(0)
public static final TextField USER_MAIL = OJFields.create(AuthenticationRequest.class);

@FinalNeverNull
@FieldOrder(1)
public static final TextField PASSWORD = OJFields.create(AuthenticationRequest.class);

public AuthenticationRequest(String pUserMail, String pPassword)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package de.adito.ojcms.rest.auth.api;

import de.adito.ojcms.beans.*;
import de.adito.ojcms.beans.annotations.FinalNeverNull;
import de.adito.ojcms.beans.annotations.*;
import de.adito.ojcms.beans.literals.fields.types.TextField;

/**
Expand All @@ -14,9 +14,12 @@
public class AuthenticationResponse extends OJBean
{
@FinalNeverNull
public static TextField TOKEN = OJFields.create(AuthenticationResponse.class);
@FieldOrder(0)
public static final TextField TOKEN = OJFields.create(AuthenticationResponse.class);

@FinalNeverNull
public static TextField NEXT_PASSWORD = OJFields.create(AuthenticationResponse.class);
@FieldOrder(1)
public static final TextField NEXT_PASSWORD = OJFields.create(AuthenticationResponse.class);

public AuthenticationResponse(String pToken, String pNextPassword)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package de.adito.ojcms.rest.auth.api;

import de.adito.ojcms.beans.*;
import de.adito.ojcms.beans.annotations.FinalNeverNull;
import de.adito.ojcms.beans.annotations.*;
import de.adito.ojcms.beans.literals.fields.types.TextField;

/**
Expand All @@ -12,9 +12,12 @@
public class RegistrationRequest extends OJBean
{
@FinalNeverNull
public static TextField USER_MAIL = OJFields.create(RegistrationRequest.class);
@FieldOrder(0)
public static final TextField USER_MAIL = OJFields.create(RegistrationRequest.class);

@FinalNeverNull
public static TextField DISPLAY_NAME = OJFields.create(RegistrationRequest.class);
@FieldOrder(1)
public static final TextField DISPLAY_NAME = OJFields.create(RegistrationRequest.class);

public RegistrationRequest(String pUserMail, String pDisplayName)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package de.adito.ojcms.rest.auth.api;

import de.adito.ojcms.beans.*;
import de.adito.ojcms.beans.annotations.FinalNeverNull;
import de.adito.ojcms.beans.annotations.*;
import de.adito.ojcms.beans.literals.fields.types.TextField;

/**
Expand All @@ -12,9 +12,12 @@
public final class RestoreAuthenticationRequest extends OJBean
{
@FinalNeverNull
public static TextField USER_MAIL = OJFields.create(RestoreAuthenticationRequest.class);
@FieldOrder(0)
public static final TextField USER_MAIL = OJFields.create(RestoreAuthenticationRequest.class);

@FinalNeverNull
public static TextField RESTORE_CODE = OJFields.create(RestoreAuthenticationRequest.class);
@FieldOrder(1)
public static final TextField RESTORE_CODE = OJFields.create(RestoreAuthenticationRequest.class);

/**
* Creates a new restore request.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package de.adito.ojcms.rest.testapplication;

import de.adito.ojcms.beans.OJFields;
import de.adito.ojcms.beans.annotations.FinalNeverNull;
import de.adito.ojcms.beans.annotations.*;
import de.adito.ojcms.beans.literals.fields.types.EnumField;
import de.adito.ojcms.rest.auth.api.RegistrationRequest;

Expand All @@ -13,6 +13,7 @@
public class RegistrationRequestForTest extends RegistrationRequest
{
@FinalNeverNull
@FieldOrder(2)
public static final EnumField<EUserRoleForTest> USER_ROLE = OJFields.create(RegistrationRequestForTest.class);

public RegistrationRequestForTest(String pUserMail, String pDisplayName, EUserRoleForTest pUserRole)
Expand Down

0 comments on commit 573f71a

Please sign in to comment.