Skip to content

Commit

Permalink
Merge pull request #99 from fujaba/feat/dto-annotation
Browse files Browse the repository at this point in the history
DTO Mapping
  • Loading branch information
Clashsoft authored Jun 12, 2021
2 parents 57df68a + 97ac422 commit 625df9d
Show file tree
Hide file tree
Showing 16 changed files with 1,146 additions and 31 deletions.
430 changes: 430 additions & 0 deletions docs/definitions/10-dtos.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/definitions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ This chapter explains the various options for defining class models.
* [Inheritance](7-inheritance.md)
* [Methods](8-methods.md)
* [EMF and Ecore files](9-emf-ecore.md)
* [DTOs](10-dtos.md)
97 changes: 71 additions & 26 deletions src/main/java/org/fulib/builder/ReflectiveClassBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,43 @@
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static org.fulib.builder.Type.MANY;
import static org.fulib.builder.Type.ONE;
import static org.fulib.builder.Type.*;

class ReflectiveClassBuilder
{
private static final String ID_PATTERN = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*";
private static final Pattern CLASS_PATTERN = Pattern.compile(ID_PATTERN + "(?:\\." + ID_PATTERN + ")*");
private final ClassModelManager manager;

static Clazz load(Class<?> classDef, ClassModelManager manager)
ReflectiveClassBuilder(ClassModelManager classModelManager)
{
this.manager = classModelManager;
}

static Clazz load(Class<?> classDef, ClassModelManager classModelManager)
{
return new ReflectiveClassBuilder(classModelManager).load(classDef);
}

Clazz load(Class<?> classDef)
{
final Clazz clazz = manager.haveClass(classDef.getSimpleName());

final DTO dto = classDef.getAnnotation(DTO.class);
if (dto != null)
{
loadDto(dto, clazz);
}

final Class<?> superClass = classDef.getSuperclass();
if (superClass != null && superClass != Object.class)
{
Expand All @@ -32,13 +53,29 @@ static Clazz load(Class<?> classDef, ClassModelManager manager)

for (final Field field : classDef.getDeclaredFields())
{
loadField(field, clazz, manager);
loadField(field, clazz, false);
}

return clazz;
}

private static void loadField(Field field, Clazz clazz, ClassModelManager manager)
private void loadDto(DTO dto, Clazz clazz)
{
final Class<?> model = dto.model();
final Set<String> include = new HashSet<>(Arrays.asList(dto.pick()));
final Set<String> exclude = new HashSet<>(Arrays.asList(dto.omit()));

for (final Field field : model.getDeclaredFields())
{
final String name = field.getName();
if ((include.isEmpty() || include.contains(name)) && !exclude.contains(name))
{
loadField(field, clazz, true);
}
}
}

private void loadField(Field field, Clazz clazz, boolean dto)
{
if (field.isSynthetic())
{
Expand All @@ -48,33 +85,40 @@ private static void loadField(Field field, Clazz clazz, ClassModelManager manage
final Link link = field.getAnnotation(Link.class);
if (link == null)
{
loadAttribute(field, clazz, manager);
loadAttribute(field, clazz, false);
}
else if (dto)
{
loadAttribute(field, clazz, true);
}
else
{
loadAssoc(field, link, clazz, manager);
loadAssoc(field, link, clazz);
}
}

private static void loadAttribute(Field field, Clazz clazz, ClassModelManager manager)
private void loadAttribute(Field field, Clazz clazz, boolean dto)
{
final String name = field.getName();
final CollectionType collectionType = getCollectionType(field.getType());
final String type = getType(field, collectionType);
final String type = dto ? STRING : getType(field, collectionType);

final Attribute attribute = manager.haveAttribute(clazz, name, type);
attribute.setCollectionType(collectionType);
attribute.setDescription(getDescription(field));
attribute.setSince(getSince(field));

final InitialValue initialValue = field.getAnnotation(InitialValue.class);
if (initialValue != null)
if (!dto)
{
attribute.setInitialization(initialValue.value());
final InitialValue initialValue = field.getAnnotation(InitialValue.class);
if (initialValue != null)
{
attribute.setInitialization(initialValue.value());
}
}
}

private static String getType(Field field, CollectionType collectionType)
private String getType(Field field, CollectionType collectionType)
{
final org.fulib.builder.reflect.Type type = field.getAnnotation(org.fulib.builder.reflect.Type.class);
if (type != null)
Expand All @@ -95,11 +139,11 @@ private static String getType(Field field, CollectionType collectionType)
}

throw new InvalidClassModelException(
String.format("%s.%s: cannot determine element type of %s", declaringClass.getSimpleName(),
field.getName(), field.getType().getSimpleName()));
String.format("%s.%s: cannot determine element type of %s", declaringClass.getSimpleName(), field.getName(),
field.getType().getSimpleName()));
}

private static String toSource(Class<?> base, Type type)
private String toSource(Class<?> base, Type type)
{
final String input = type.getTypeName();
final Matcher matcher = CLASS_PATTERN.matcher(input);
Expand All @@ -116,7 +160,7 @@ private static String toSource(Class<?> base, Type type)
return sb.toString();
}

private static void toSource(Class<?> base, String className, StringBuilder out)
private void toSource(Class<?> base, String className, StringBuilder out)
{
switch (className)
{
Expand Down Expand Up @@ -146,7 +190,8 @@ else if (resolvedPackage == base.getPackage())
{
out.append(resolved.getSimpleName());
}
else if (resolved.getEnclosingClass() != null && ClassModelDecorator.class.isAssignableFrom(resolved.getEnclosingClass()))
else if (resolved.getEnclosingClass() != null && ClassModelDecorator.class.isAssignableFrom(
resolved.getEnclosingClass()))
{
// resolved is nested class within another GenModel
out
Expand All @@ -167,7 +212,7 @@ else if (resolved.getEnclosingClass() != null && ClassModelDecorator.class.isAss
}
}

private static CollectionType getCollectionType(Class<?> type)
private CollectionType getCollectionType(Class<?> type)
{
if (!Collection.class.isAssignableFrom(type))
{
Expand All @@ -184,7 +229,7 @@ private static CollectionType getCollectionType(Class<?> type)
return CollectionType.of(collectionType);
}

private static void loadAssoc(Field field, Link link, Clazz clazz, ClassModelManager manager)
private void loadAssoc(Field field, Link link, Clazz clazz)
{
final Class<?> owner = field.getDeclaringClass();
final String name = field.getName();
Expand Down Expand Up @@ -214,7 +259,7 @@ private static void loadAssoc(Field field, Link link, Clazz clazz, ClassModelMan
role.setSince(getSince(field));
}

private static void validateTargetClass(Class<?> owner, String name, Class<?> other)
private void validateTargetClass(Class<?> owner, String name, Class<?> other)
{
if (owner.getPackage() != other.getPackage())
{
Expand All @@ -225,7 +270,7 @@ private static void validateTargetClass(Class<?> owner, String name, Class<?> ot
}
}

private static void validateLinkTarget(Class<?> owner, String name, String otherName, Class<?> other)
private void validateLinkTarget(Class<?> owner, String name, String otherName, Class<?> other)
{
final Field targetField;
try
Expand Down Expand Up @@ -265,7 +310,7 @@ private static void validateLinkTarget(Class<?> owner, String name, String other
}
}

private static Class<?> getOther(Field field, CollectionType collectionType)
private Class<?> getOther(Field field, CollectionType collectionType)
{
final org.fulib.builder.reflect.Type type = field.getAnnotation(org.fulib.builder.reflect.Type.class);
if (type != null)
Expand Down Expand Up @@ -293,7 +338,7 @@ private static Class<?> getOther(Field field, CollectionType collectionType)
field.getName(), field.getType().getSimpleName()));
}

private static Class<?> getSiblingClass(Field field, String simpleSiblingName)
private Class<?> getSiblingClass(Field field, String simpleSiblingName)
{
final Class<?> declaringClass = field.getDeclaringClass();
final String name = declaringClass.getName();
Expand All @@ -312,13 +357,13 @@ private static Class<?> getSiblingClass(Field field, String simpleSiblingName)
}
}

private static String getDescription(Field field)
private String getDescription(Field field)
{
final Description annotation = field.getAnnotation(Description.class);
return annotation != null ? annotation.value() : null;
}

private static String getSince(Field field)
private String getSince(Field field)
{
final Since annotation = field.getAnnotation(Since.class);
return annotation != null ? annotation.value() : null;
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/org/fulib/builder/reflect/DTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.fulib.builder.reflect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Allows creating Data Transfer Object (DTO) classes by copying attributes from a model class.
* Associations are automatically converted to String attributes meant for holding the ID of the link target.
*
* @since 1.6
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DTO
{
/**
* @return the model class to copy attributes from
*/
Class<?> model();

/**
* @return the names of fields that should be included. Ignored if empty.
*/
String[] pick() default {};

/**
* @return the names of fields that should be excluded
*/
String[] omit() default {};
}
57 changes: 57 additions & 0 deletions src/test/java/org/fulib/builder/ReflectiveClassBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -317,4 +317,61 @@ public void invalidLinkClassPackage()
assertThat(ex.getMessage(), equalTo(
"InvalidLinkClassPackage.student: invalid link: target class Student (org.fulib.builder.model) must be in the same package (org.fulib.builder)"));
}

@DTO(model = Person.class)
class PersonDto
{}

@Test
public void dto()
{
final ClassModelManager cmm = new ClassModelManager();
final Clazz personDto = ReflectiveClassBuilder.load(PersonDto.class, cmm);

assertThat(personDto.getRole("links"), nullValue());

final Attribute name = personDto.getAttribute("name");
assertThat(name.getType(), is(Type.STRING));

final Attribute friends = personDto.getAttribute("friends");
assertThat(friends.getType(), is(Type.STRING));
assertThat(friends.getCollectionType(), is(CollectionType.ArrayList));

final Attribute dateOfBirth = personDto.getAttribute("dateOfBirth");
assertThat(dateOfBirth.getType(), is("import(java.util.Date)"));
}

@DTO(model = Person.class, pick = { "friends" })
class PersonLinksDto
{}

@DTO(model = Person.class, omit = { "friends" })
class PersonAttributesDto
{}

@Test
public void dtoPickOmit()
{
final ClassModelManager cmm = new ClassModelManager();
final Clazz personLinksDto = ReflectiveClassBuilder.load(PersonLinksDto.class, cmm);

assertThat(personLinksDto.getAttribute("name"), nullValue());
assertThat(personLinksDto.getAttribute("dateOfBirth"), nullValue());
assertThat(personLinksDto.getRole("links"), nullValue());

final Attribute friends = personLinksDto.getAttribute("friends");
assertThat(friends.getType(), is(Type.STRING));
assertThat(friends.getCollectionType(), is(CollectionType.ArrayList));

final Clazz personAttributesDto = ReflectiveClassBuilder.load(PersonAttributesDto.class, cmm);

final Attribute name = personAttributesDto.getAttribute("name");
assertThat(name.getType(), is(Type.STRING));

final Attribute dateOfBirth = personAttributesDto.getAttribute("dateOfBirth");
assertThat(dateOfBirth.getType(), is("import(java.util.Date)"));

assertThat(personAttributesDto.getAttribute("links"), nullValue());
assertThat(personAttributesDto.getRole("links"), nullValue());
}
}
7 changes: 2 additions & 5 deletions test/src/gen/java/org/fulib/docs/GenModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@
import it.unimi.dsi.fastutil.ints.IntArrayList;
import org.fulib.builder.ClassModelDecorator;
import org.fulib.builder.ClassModelManager;
import org.fulib.builder.reflect.Type;
import org.fulib.builder.reflect.Description;
import org.fulib.builder.reflect.InitialValue;
import org.fulib.builder.reflect.Link;
import org.fulib.builder.reflect.Since;
import org.fulib.builder.reflect.*;

import java.util.ArrayList;
import java.util.List;

@SuppressWarnings({ "unused", "InnerClassMayBeStatic" })
public class GenModel implements ClassModelDecorator
{
// start_code_fragment: docs.GenModel.Person
Expand Down
28 changes: 28 additions & 0 deletions test/src/gen/java/org/fulib/docs/dtos/dto/GenDtos.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.fulib.docs.dtos.dto;

import org.fulib.builder.ClassModelDecorator;
import org.fulib.builder.ClassModelManager;
import org.fulib.builder.Type;
import org.fulib.builder.reflect.DTO;
import org.fulib.docs.dtos.model.GenModel;

// start_code_fragment: docs.dtos.GenDtos
public class GenDtos implements ClassModelDecorator
{
@DTO(model = GenModel.User.class, omit = { "id" })
class UserDto
{}

@DTO(model = GenModel.Address.class, pick = { "city", "street" })
class AddressDto
{}

@Override
public void decorate(ClassModelManager m)
{
// This omits PropertyChangeListeners etc. from the generated code
m.getClassModel().setDefaultPropertyStyle(Type.POJO);
m.haveNestedClasses(GenDtos.class);
}
}
// end_code_fragment:
Loading

0 comments on commit 625df9d

Please sign in to comment.