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

DTO Mapping #99

Merged
merged 5 commits into from
Jun 12, 2021
Merged
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
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