From c73ca5ec4733bcfd2e5f5daf37fb6a0e73af1f43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Suszy=C5=84ski=20Krzysztof?= Date: Fri, 27 Apr 2018 19:00:46 +0200 Subject: [PATCH 1/2] Promiscuous and Quiet modes --- README.md | 166 +++++++++------ pom.xml | 2 +- .../utils/stringify/ObjectStringifier.java | 21 +- .../utils/stringify/annotation/Inspect.java | 21 -- .../configuration/AlwaysTruePredicate.java | 14 ++ .../stringify/configuration/BeanFactory.java | 9 + .../stringify/configuration/DisplayNull.java | 27 +++ .../stringify/configuration/DoNotInspect.java | 26 +++ .../stringify/configuration/Inspect.java | 28 +++ .../utils/stringify/configuration/Mode.java | 16 ++ .../stringify/configuration/Predicate.java | 23 ++ .../package-info.java | 2 +- .../utils/stringify/impl/ClassLocator.java | 3 +- .../stringify/impl/InspectFieldPredicate.java | 11 + .../utils/stringify/impl/InspectingField.java | 10 + .../impl/InspectingFieldFactory.java | 55 +++++ .../stringify/impl/JPALazyCheckerFacade.java | 3 +- .../PromiscuousInspectFieldPredicate.java | 46 ++++ .../impl/QuietInspectFieldPredicate.java | 39 ++++ .../stringify/impl/ReflectionBeanFactory.java | 53 +++++ .../stringify/impl/ToStringResolver.java | 174 ++------------- .../impl/ToStringResolverFactory.java | 9 + .../impl/ToStringResolverFactoryImpl.java | 12 ++ .../stringify/impl/ToStringResolverImpl.java | 201 ++++++++++++++++++ .../wavesoftware/utils/stringify/Account.java | 11 + .../wavesoftware/utils/stringify/Earth.java | 3 +- .../utils/stringify/IsInDevelopment.java | 12 ++ .../pl/wavesoftware/utils/stringify/Moon.java | 6 +- .../stringify/ObjectStringifierTest.java | 131 +++++++++++- .../wavesoftware/utils/stringify/Person.java | 25 +++ .../wavesoftware/utils/stringify/Planet.java | 9 +- .../utils/stringify/PlanetSystem.java | 2 +- .../stringify/ProductionEnvironment.java | 22 ++ .../utils/stringify/SimpleUser.java | 19 ++ .../utils/stringify/TestRepository.java | 53 +++++ .../pl/wavesoftware/utils/stringify/User.java | 17 ++ 36 files changed, 1021 insertions(+), 260 deletions(-) delete mode 100644 src/main/java/pl/wavesoftware/utils/stringify/annotation/Inspect.java create mode 100644 src/main/java/pl/wavesoftware/utils/stringify/configuration/AlwaysTruePredicate.java create mode 100644 src/main/java/pl/wavesoftware/utils/stringify/configuration/BeanFactory.java create mode 100644 src/main/java/pl/wavesoftware/utils/stringify/configuration/DisplayNull.java create mode 100644 src/main/java/pl/wavesoftware/utils/stringify/configuration/DoNotInspect.java create mode 100644 src/main/java/pl/wavesoftware/utils/stringify/configuration/Inspect.java create mode 100644 src/main/java/pl/wavesoftware/utils/stringify/configuration/Mode.java create mode 100644 src/main/java/pl/wavesoftware/utils/stringify/configuration/Predicate.java rename src/main/java/pl/wavesoftware/utils/stringify/{annotation => configuration}/package-info.java (78%) create mode 100644 src/main/java/pl/wavesoftware/utils/stringify/impl/InspectFieldPredicate.java create mode 100644 src/main/java/pl/wavesoftware/utils/stringify/impl/InspectingField.java create mode 100644 src/main/java/pl/wavesoftware/utils/stringify/impl/InspectingFieldFactory.java create mode 100644 src/main/java/pl/wavesoftware/utils/stringify/impl/PromiscuousInspectFieldPredicate.java create mode 100644 src/main/java/pl/wavesoftware/utils/stringify/impl/QuietInspectFieldPredicate.java create mode 100644 src/main/java/pl/wavesoftware/utils/stringify/impl/ReflectionBeanFactory.java create mode 100644 src/main/java/pl/wavesoftware/utils/stringify/impl/ToStringResolverFactory.java create mode 100644 src/main/java/pl/wavesoftware/utils/stringify/impl/ToStringResolverFactoryImpl.java create mode 100644 src/main/java/pl/wavesoftware/utils/stringify/impl/ToStringResolverImpl.java create mode 100644 src/test/java/pl/wavesoftware/utils/stringify/Account.java create mode 100644 src/test/java/pl/wavesoftware/utils/stringify/IsInDevelopment.java create mode 100644 src/test/java/pl/wavesoftware/utils/stringify/Person.java create mode 100644 src/test/java/pl/wavesoftware/utils/stringify/ProductionEnvironment.java create mode 100644 src/test/java/pl/wavesoftware/utils/stringify/SimpleUser.java create mode 100644 src/test/java/pl/wavesoftware/utils/stringify/User.java diff --git a/README.md b/README.md index 36408ac..5c633b4 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,103 @@ - -# Stringify Object for Java - -[![Build Status](https://travis-ci.org/wavesoftware/java-stringify-object.svg?branch=master)](https://travis-ci.org/wavesoftware/java-stringify-object) [![Quality Gate](https://sonar.wavesoftware.pl/api/badges/gate?key=pl.wavesoftware.utils:stringify-object)](https://sonar.wavesoftware.pl/dashboard/index/pl.wavesoftware.utils:stringify-object) [![Coverage Status](https://coveralls.io/repos/github/wavesoftware/java-stringify-object/badge.svg?branch=master)](https://coveralls.io/github/wavesoftware/java-stringify-object?branch=master) -[![Maven Central](https://img.shields.io/maven-central/v/pl.wavesoftware.utils/stringify-object.svg)](https://bintray.com/bintray/jcenter/pl.wavesoftware.utils%3Astringify-object) - - -A utility to safely inspect any Java Object as String representation. It's best to be used with JPA entity model to be dumped -to log files. - -It utilize `@Inspect` annotation to indicate which fields - should be displayed. It has support for cycles, and Hibernate lazy loaded elements. - - ## Usage - -```java -// define fields to inspect +# Stringify Object for Java + +[![Build Status](https://travis-ci.org/wavesoftware/java-stringify-object.svg?branch=master)](https://travis-ci.org/wavesoftware/java-stringify-object) [![Quality Gate](https://sonar.wavesoftware.pl/api/badges/gate?key=pl.wavesoftware.utils:stringify-object)](https://sonar.wavesoftware.pl/dashboard/index/pl.wavesoftware.utils:stringify-object) [![Coverage Status](https://coveralls.io/repos/github/wavesoftware/java-stringify-object/badge.svg?branch=master)](https://coveralls.io/github/wavesoftware/java-stringify-object?branch=master) [![Maven Central](https://img.shields.io/maven-central/v/pl.wavesoftware.utils/stringify-object.svg)](https://bintray.com/bintray/jcenter/pl.wavesoftware.utils%3Astringify-object) + + +A utility to safely inspect any Java Object as String representation. It's best to be used with domain model (also with JPA entities) with intention to dump those entities as text to log files. + +It runs in two modes: `PROMISCUOUS` (by default) and `QUIET`. In `PROMISCUOUS` mode every defined field is automatically inspected, unless the field is annotated with `@DoNotInspect` annotation. +In `QUIET` mode only fields annotated with `@Inspect` will gets inspected. + +This library has proper support for object graph cycles, and JPA (Hibernate) lazy loaded elements. + + ## Usage + + ### In Promiscuous mode + +```java +// In PROMISCUOUS mode define fields to exclude class Person { + private int id; + @DisplayNull + private Person parent; + private List childs; + private Account account; + @Inspect(conditionally = IsInDevelopment.class) + private String password; + @DoNotInspect + private String ignored; +} + +// inspect an object +List people = query.getResultList(); +ObjectStringifier stringifier = new ObjectStringifier(people); +stringifier.setMode(Mode.PROMISCUOUS); +// stringifier.setBeanFactory(...); +assert ", childs=[], " + + "account=⁂Lazy>".equals(stringifier.toString()); +``` + + ### In Quiet mode +```java +// In QUIET mode define fields to inspect +class Person { @Inspect private int id; - @Inspect private Person parent; + @Inspect @DisplayNull private Person parent; @Inspect private List childs; @Inspect private Account account; + private String ignored; } - -// inspect an object -List people = query.getResultList(); -ObjectStringifier stringifier = new ObjectStringifier(people); -assert ", childs=[], " - + "account=⁂Lazy>".equals(stringifier.toString()); -``` - -## Features - - * String representation of any Java class - * Fine tuning of which fields to display - * Support for cycles in object graph - `(↻)` is displayed instead - * Support for Hibernate lazy loaded entities - `⁂Lazy` is displayed instead - -## vs. Lombok @ToString - -Stringify Object for Java is designed for **slightly different** use case then Lombok. - -Lombok `@ToString` is designed to quickly inspect fields of simple objects by generating static simple implementation of this mechanism. - -Stringify Object for Java is designed to inspect complex objects that can have cycles and can be managed by JPA provider like Hibernate (introducing Lazy Loading problems). - -#### Pros of Lombok vs Stringify Object - - * Lombok is **fast** - it's statically generated code without using Reflection API. - * Lombok is **easy** - it's zero configuration in most cases. - -#### Cons of Lombok vs Stringify Object - - * Lombok can't **detect cycles** is object graph, which implies `StackOverflowException` being thrown in that case - * Lombok can't detect a **lazy loaded entities**, which leads to force loading it from JPA by invoking SQL statements. It's typical **n+1 problem**, but with nasty consequences - your `toString()` method is invoking SQL without your knowledge!! - -## Requirements - - * JDK >= 7 - * [EID Exceptions](https://github.com/wavesoftware/java-eid-exceptions) - -### Contributing - -Contributions are welcome! - -To contribute, follow the standard [git flow](http://danielkummer.github.io/git-flow-cheatsheet/) of: - -1. Fork it -1. Create your feature branch (`git checkout -b feature/my-new-feature`) -1. Commit your changes (`git commit -am 'Add some feature'`) -1. Push to the branch (`git push origin feature/my-new-feature`) -1. Create new Pull Request - + +// inspect an object +List people = query.getResultList(); +ObjectStringifier stringifier = new ObjectStringifier(people); +stringifier.setMode(Mode.QUIET); +assert ", childs=[], " + + "account=⁂Lazy>".equals(stringifier.toString()); +``` + +## Features + + * String representation of any Java class in two modes `PROMISCUOUS` and `QUIET` + * Fine tuning of which fields to display + * Support for cycles in object graph - `(↻)` is displayed instead + * Support for Hibernate lazy loaded entities - `⁂Lazy` is displayed instead + +## vs. Lombok @ToString + +Stringify Object for Java is designed for **slightly different** use case then Lombok. + +Lombok `@ToString` is designed to quickly inspect fields of simple objects by generating static simple implementation of this mechanism. + +Stringify Object for Java is designed to inspect complex objects that can have cycles and can be managed by JPA provider like Hibernate (introducing Lazy Loading problems). + +#### Pros of Lombok vs Stringify Object + + * Lombok is **fast** - it's statically generated code without using Reflection API. + * Lombok is **easy** - it's zero configuration in most cases. + +#### Cons of Lombok vs Stringify Object + + * Lombok can't **detect cycles** is object graph, which implies `StackOverflowException` being thrown in that case + * Lombok can't detect a **lazy loaded entities**, which leads to force loading it from JPA by invoking SQL statements. It's typical **n+1 problem**, but with nasty consequences - your `toString()` method is invoking SQL without your knowledge!! + +## Dependencies + + * Java >= 7 + * [EID Exceptions](https://github.com/wavesoftware/java-eid-exceptions) library + +### Contributing + +Contributions are welcome! + +To contribute, follow the standard [git flow](http://danielkummer.github.io/git-flow-cheatsheet/) of: + +1. Fork it +1. Create your feature branch (`git checkout -b feature/my-new-feature`) +1. Commit your changes (`git commit -am 'Add some feature'`) +1. Push to the branch (`git push origin feature/my-new-feature`) +1. Create new Pull Request + Even if you can't contribute code, if you have an idea for an improvement please open an [issue](https://github.com/wavesoftware/java-stringify-object/issues). diff --git a/pom.xml b/pom.xml index 48f0a0f..936e9d1 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ pl.wavesoftware.utils stringify-object - 0.1.1-SNAPSHOT + 1.0.0-SNAPSHOT jar Stringify Object for Java diff --git a/src/main/java/pl/wavesoftware/utils/stringify/ObjectStringifier.java b/src/main/java/pl/wavesoftware/utils/stringify/ObjectStringifier.java index e9c3bff..650d90d 100644 --- a/src/main/java/pl/wavesoftware/utils/stringify/ObjectStringifier.java +++ b/src/main/java/pl/wavesoftware/utils/stringify/ObjectStringifier.java @@ -1,23 +1,40 @@ package pl.wavesoftware.utils.stringify; import lombok.RequiredArgsConstructor; -import pl.wavesoftware.utils.stringify.annotation.Inspect; +import lombok.Setter; +import pl.wavesoftware.utils.stringify.configuration.BeanFactory; +import pl.wavesoftware.utils.stringify.configuration.Inspect; +import pl.wavesoftware.utils.stringify.configuration.Mode; import pl.wavesoftware.utils.stringify.impl.ToStringResolver; +import pl.wavesoftware.utils.stringify.impl.ToStringResolverFactory; +import pl.wavesoftware.utils.stringify.impl.ToStringResolverFactoryImpl; /** * @author Krzysztof Suszyński * @since 2018-04-18 */ +@Setter @RequiredArgsConstructor public final class ObjectStringifier { + private static final ToStringResolverFactory RESOLVER_FACTORY = + new ToStringResolverFactoryImpl(); private final Object target; + private Mode mode = Mode.DEFAULT_MODE; + private BeanFactory beanFactory; + /** * Creates string representation of given object using {@link Inspect} on given fields. * * @return a string representation of given object */ public CharSequence stringify() { - return new ToStringResolver(target).resolve(); + ToStringResolver resolver = RESOLVER_FACTORY + .create(target) + .withMode(mode); + if (beanFactory != null) { + resolver = resolver.withBeanFactory(beanFactory); + } + return resolver.resolve(); } @Override diff --git a/src/main/java/pl/wavesoftware/utils/stringify/annotation/Inspect.java b/src/main/java/pl/wavesoftware/utils/stringify/annotation/Inspect.java deleted file mode 100644 index ad0bc8f..0000000 --- a/src/main/java/pl/wavesoftware/utils/stringify/annotation/Inspect.java +++ /dev/null @@ -1,21 +0,0 @@ -package pl.wavesoftware.utils.stringify.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a field to be inspected to String representation of parent object. - * - * @author Krzysztof Suszyński - * @since 2018-04-18 - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface Inspect { - /** - * Should show if field contain a NULL value? - */ - boolean showNull() default false; -} diff --git a/src/main/java/pl/wavesoftware/utils/stringify/configuration/AlwaysTruePredicate.java b/src/main/java/pl/wavesoftware/utils/stringify/configuration/AlwaysTruePredicate.java new file mode 100644 index 0000000..8505db5 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/stringify/configuration/AlwaysTruePredicate.java @@ -0,0 +1,14 @@ +package pl.wavesoftware.utils.stringify.configuration; + +import java.lang.reflect.Field; + +/** + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +public final class AlwaysTruePredicate implements Predicate { + @Override + public boolean test(Field field) { + return true; + } +} diff --git a/src/main/java/pl/wavesoftware/utils/stringify/configuration/BeanFactory.java b/src/main/java/pl/wavesoftware/utils/stringify/configuration/BeanFactory.java new file mode 100644 index 0000000..413b1c7 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/stringify/configuration/BeanFactory.java @@ -0,0 +1,9 @@ +package pl.wavesoftware.utils.stringify.configuration; + +/** + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +public interface BeanFactory { + T create(Class contractClass); +} diff --git a/src/main/java/pl/wavesoftware/utils/stringify/configuration/DisplayNull.java b/src/main/java/pl/wavesoftware/utils/stringify/configuration/DisplayNull.java new file mode 100644 index 0000000..1816139 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/stringify/configuration/DisplayNull.java @@ -0,0 +1,27 @@ +package pl.wavesoftware.utils.stringify.configuration; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation can be used to specify if given field should be displayed even + * if it holds a null value. + * + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface DisplayNull { + + boolean BY_DEFAULT = false; + + /** + * Do show null value on field annotated with this annotation + * + * @return true if should display + */ + boolean value() default true; +} diff --git a/src/main/java/pl/wavesoftware/utils/stringify/configuration/DoNotInspect.java b/src/main/java/pl/wavesoftware/utils/stringify/configuration/DoNotInspect.java new file mode 100644 index 0000000..db42a34 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/stringify/configuration/DoNotInspect.java @@ -0,0 +1,26 @@ +package pl.wavesoftware.utils.stringify.configuration; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; + +/** + * When running in {@link Mode#PROMISCUOUS} this annotation can be used to exclude a + * field from inspection. + * + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface DoNotInspect { + /** + * If given this predicate will be used to conditionally check if annotated field should not + * be inspected. This can lead to implementing conditional logic of field inspection. + * + * @return a class of predicate to be used to determine if field should not be inspected + */ + Class> conditionally() default AlwaysTruePredicate.class; +} diff --git a/src/main/java/pl/wavesoftware/utils/stringify/configuration/Inspect.java b/src/main/java/pl/wavesoftware/utils/stringify/configuration/Inspect.java new file mode 100644 index 0000000..ca16ade --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/stringify/configuration/Inspect.java @@ -0,0 +1,28 @@ +package pl.wavesoftware.utils.stringify.configuration; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; + +/** + * If {@link Mode} is set to {@link Mode#QUIET} (by default), this annotation + * marks a field to be inspected to String representation of parent object. + *

+ * If {@link Mode} is set to {@link Mode#PROMISCUOUS} this annotation has no function. + * + * @author Krzysztof Suszyński + * @since 2018-04-18 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Inspect { + /** + * If given this predicate will be used to conditionally check if annotated field should + * be inspected. This can lead to implementing conditional logic of field inspection. + * + * @return a class of predicate to be used to determine if field should be inspected + */ + Class> conditionally() default AlwaysTruePredicate.class; +} diff --git a/src/main/java/pl/wavesoftware/utils/stringify/configuration/Mode.java b/src/main/java/pl/wavesoftware/utils/stringify/configuration/Mode.java new file mode 100644 index 0000000..33eb49d --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/stringify/configuration/Mode.java @@ -0,0 +1,16 @@ +package pl.wavesoftware.utils.stringify.configuration; + +/** + * A mode of operation. In {@link #PROMISCUOUS} mode every defined field is + * automatically inspected unless the field is annotated with {@link DoNotInspect} annotation. + *

+ * In {@link #QUIET} mode only fields annotated with {@link Inspect} will get inspected. + * + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +public enum Mode { + PROMISCUOUS, QUIET; + + public static final Mode DEFAULT_MODE = PROMISCUOUS; +} diff --git a/src/main/java/pl/wavesoftware/utils/stringify/configuration/Predicate.java b/src/main/java/pl/wavesoftware/utils/stringify/configuration/Predicate.java new file mode 100644 index 0000000..11c1197 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/stringify/configuration/Predicate.java @@ -0,0 +1,23 @@ +package pl.wavesoftware.utils.stringify.configuration; + +/** + * Represents a predicate (boolean-valued function) of one argument. + * + *

This is a functional interface + * whose functional method is {@link #test(Object)}. + * + * @param the type of the input to the predicate + * + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +public interface Predicate { + /** + * Evaluates this predicate on the given argument. + * + * @param t the input argument + * @return {@code true} if the input argument matches the predicate, + * otherwise {@code false} + */ + boolean test(T t); +} diff --git a/src/main/java/pl/wavesoftware/utils/stringify/annotation/package-info.java b/src/main/java/pl/wavesoftware/utils/stringify/configuration/package-info.java similarity index 78% rename from src/main/java/pl/wavesoftware/utils/stringify/annotation/package-info.java rename to src/main/java/pl/wavesoftware/utils/stringify/configuration/package-info.java index f883908..24dc5c2 100644 --- a/src/main/java/pl/wavesoftware/utils/stringify/annotation/package-info.java +++ b/src/main/java/pl/wavesoftware/utils/stringify/configuration/package-info.java @@ -3,6 +3,6 @@ * @since 20.04.18 */ @ParametersAreNonnullByDefault -package pl.wavesoftware.utils.stringify.annotation; +package pl.wavesoftware.utils.stringify.configuration; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/pl/wavesoftware/utils/stringify/impl/ClassLocator.java b/src/main/java/pl/wavesoftware/utils/stringify/impl/ClassLocator.java index 6dc64dc..c6cc4f5 100644 --- a/src/main/java/pl/wavesoftware/utils/stringify/impl/ClassLocator.java +++ b/src/main/java/pl/wavesoftware/utils/stringify/impl/ClassLocator.java @@ -1,5 +1,6 @@ package pl.wavesoftware.utils.stringify.impl; +import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import javax.annotation.Nullable; @@ -10,7 +11,7 @@ * @author Krzysztof Suszyński * @since 2018-04-18 */ -@RequiredArgsConstructor +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) final class ClassLocator { private final String fqcn; diff --git a/src/main/java/pl/wavesoftware/utils/stringify/impl/InspectFieldPredicate.java b/src/main/java/pl/wavesoftware/utils/stringify/impl/InspectFieldPredicate.java new file mode 100644 index 0000000..1ece32c --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/stringify/impl/InspectFieldPredicate.java @@ -0,0 +1,11 @@ +package pl.wavesoftware.utils.stringify.impl; + +import java.lang.reflect.Field; + +/** + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +interface InspectFieldPredicate { + boolean shouldInspect(Field field); +} diff --git a/src/main/java/pl/wavesoftware/utils/stringify/impl/InspectingField.java b/src/main/java/pl/wavesoftware/utils/stringify/impl/InspectingField.java new file mode 100644 index 0000000..8e5e979 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/stringify/impl/InspectingField.java @@ -0,0 +1,10 @@ +package pl.wavesoftware.utils.stringify.impl; + +/** + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +public interface InspectingField { + boolean shouldInspect(); + boolean showNull(); +} diff --git a/src/main/java/pl/wavesoftware/utils/stringify/impl/InspectingFieldFactory.java b/src/main/java/pl/wavesoftware/utils/stringify/impl/InspectingFieldFactory.java new file mode 100644 index 0000000..6281f55 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/stringify/impl/InspectingFieldFactory.java @@ -0,0 +1,55 @@ +package pl.wavesoftware.utils.stringify.impl; + +import lombok.RequiredArgsConstructor; +import pl.wavesoftware.utils.stringify.configuration.DisplayNull; +import pl.wavesoftware.utils.stringify.configuration.Mode; +import pl.wavesoftware.utils.stringify.configuration.BeanFactory; + +import java.lang.reflect.Field; + +/** + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +@RequiredArgsConstructor +final class InspectingFieldFactory { + private final Mode mode; + + InspectingField create(Field field, + BeanFactory beanFactory) { + return new InspectingFieldImpl(field, createPredicate(beanFactory)); + } + + private InspectFieldPredicate createPredicate(BeanFactory beanFactory) { + if (mode == Mode.PROMISCUOUS) { + return new PromiscuousInspectFieldPredicate(beanFactory); + } else { + return new QuietInspectFieldPredicate(beanFactory); + } + } + + @RequiredArgsConstructor + private class InspectingFieldImpl implements InspectingField { + private final Field field; + private final InspectFieldPredicate predicate; + + @Override + public boolean shouldInspect() { + return technically() && predicate.shouldInspect(field); + } + + private boolean technically() { + return !field.isEnumConstant() && !field.isSynthetic(); + } + + @Override + public boolean showNull() { + DisplayNull displayNull = field.getAnnotation(DisplayNull.class); + if (displayNull != null) { + return displayNull.value(); + } else { + return DisplayNull.BY_DEFAULT; + } + } + } +} diff --git a/src/main/java/pl/wavesoftware/utils/stringify/impl/JPALazyCheckerFacade.java b/src/main/java/pl/wavesoftware/utils/stringify/impl/JPALazyCheckerFacade.java index 68172fa..c2ccfcd 100644 --- a/src/main/java/pl/wavesoftware/utils/stringify/impl/JPALazyCheckerFacade.java +++ b/src/main/java/pl/wavesoftware/utils/stringify/impl/JPALazyCheckerFacade.java @@ -1,12 +1,13 @@ package pl.wavesoftware.utils.stringify.impl; +import lombok.AccessLevel; import lombok.NoArgsConstructor; /** * @author Krzysztof Suszyński * @since 2018-04-18 */ -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PACKAGE) final class JPALazyCheckerFacade implements JPALazyChecker { private static final JPALazyChecker NOOP_CHECKER = new JPALazyChecker() { @Override diff --git a/src/main/java/pl/wavesoftware/utils/stringify/impl/PromiscuousInspectFieldPredicate.java b/src/main/java/pl/wavesoftware/utils/stringify/impl/PromiscuousInspectFieldPredicate.java new file mode 100644 index 0000000..2e1053a --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/stringify/impl/PromiscuousInspectFieldPredicate.java @@ -0,0 +1,46 @@ +package pl.wavesoftware.utils.stringify.impl; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import pl.wavesoftware.utils.stringify.configuration.AlwaysTruePredicate; +import pl.wavesoftware.utils.stringify.configuration.DoNotInspect; +import pl.wavesoftware.utils.stringify.configuration.Inspect; +import pl.wavesoftware.utils.stringify.configuration.Predicate; +import pl.wavesoftware.utils.stringify.configuration.BeanFactory; + +import java.lang.reflect.Field; + +/** + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +final class PromiscuousInspectFieldPredicate implements InspectFieldPredicate { + private final BeanFactory beanFactory; + + @Override + public boolean shouldInspect(Field field) { + DoNotInspect doNotInspect = field.getAnnotation(DoNotInspect.class); + if (doNotInspect != null) { + return shouldInspect(field, doNotInspect); + } else { + Inspect inspect = field.getAnnotation(Inspect.class); + if (inspect != null && inspect.conditionally() != AlwaysTruePredicate.class) { + Predicate predicate = beanFactory.create(inspect.conditionally()); + return predicate.test(field); + } else { + return true; + } + } + } + + private boolean shouldInspect(Field field, DoNotInspect doNotInspect) { + Class> predicateClass = doNotInspect.conditionally(); + if (predicateClass == AlwaysTruePredicate.class) { + return false; + } else { + Predicate predicate = beanFactory.create(predicateClass); + return !predicate.test(field); + } + } +} diff --git a/src/main/java/pl/wavesoftware/utils/stringify/impl/QuietInspectFieldPredicate.java b/src/main/java/pl/wavesoftware/utils/stringify/impl/QuietInspectFieldPredicate.java new file mode 100644 index 0000000..e6929dd --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/stringify/impl/QuietInspectFieldPredicate.java @@ -0,0 +1,39 @@ +package pl.wavesoftware.utils.stringify.impl; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import pl.wavesoftware.utils.stringify.configuration.AlwaysTruePredicate; +import pl.wavesoftware.utils.stringify.configuration.Inspect; +import pl.wavesoftware.utils.stringify.configuration.Predicate; +import pl.wavesoftware.utils.stringify.configuration.BeanFactory; + +import java.lang.reflect.Field; + +/** + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +final class QuietInspectFieldPredicate implements InspectFieldPredicate { + private final BeanFactory beanFactory; + + @Override + public boolean shouldInspect(Field field) { + Inspect inspect = field.getAnnotation(Inspect.class); + if (inspect != null) { + return shouldInspect(field, inspect); + } else { + return false; + } + } + + private boolean shouldInspect(Field field, Inspect inspect) { + Class> predicateClass = inspect.conditionally(); + if (predicateClass == AlwaysTruePredicate.class) { + return true; + } else { + Predicate predicate = beanFactory.create(predicateClass); + return predicate.test(field); + } + } +} diff --git a/src/main/java/pl/wavesoftware/utils/stringify/impl/ReflectionBeanFactory.java b/src/main/java/pl/wavesoftware/utils/stringify/impl/ReflectionBeanFactory.java new file mode 100644 index 0000000..7fbbe2f --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/stringify/impl/ReflectionBeanFactory.java @@ -0,0 +1,53 @@ +package pl.wavesoftware.utils.stringify.impl; + +import pl.wavesoftware.eid.utils.EidPreconditions; +import pl.wavesoftware.utils.stringify.configuration.BeanFactory; + +import javax.annotation.Nonnull; +import java.util.HashMap; +import java.util.Map; + +import static pl.wavesoftware.eid.utils.EidPreconditions.tryToExecute; + +/** + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +final class ReflectionBeanFactory implements BeanFactory { + private final Map, Object> created = + new HashMap<>(); + + @Override + public T create(final Class contractClass) { + if (isCached(contractClass)) { + return getFromCache(contractClass); + } + T predicate = instantinate(contractClass); + put(contractClass, predicate); + return predicate; + } + + @SuppressWarnings("unchecked") + private T getFromCache(final Class contractClass) { + Object bean = created.get(contractClass); + return (T) bean; + } + + private void put(Class contractClass, T bean) { + created.put(contractClass, bean); + } + + private boolean isCached(Class contractClass) { + return created.containsKey(contractClass); + } + + private T instantinate(final Class contractClass) { + return tryToExecute(new EidPreconditions.UnsafeSupplier() { + @Override + @Nonnull + public T get() throws IllegalAccessException, InstantiationException { + return contractClass.newInstance(); + } + }, "20180427:171402"); + } +} diff --git a/src/main/java/pl/wavesoftware/utils/stringify/impl/ToStringResolver.java b/src/main/java/pl/wavesoftware/utils/stringify/impl/ToStringResolver.java index 271a99a..12a1e19 100644 --- a/src/main/java/pl/wavesoftware/utils/stringify/impl/ToStringResolver.java +++ b/src/main/java/pl/wavesoftware/utils/stringify/impl/ToStringResolver.java @@ -1,173 +1,33 @@ package pl.wavesoftware.utils.stringify.impl; -import pl.wavesoftware.eid.utils.EidPreconditions; -import pl.wavesoftware.utils.stringify.annotation.Inspect; - -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.IdentityHashMap; -import java.util.LinkedHashMap; -import java.util.Map; - -import static pl.wavesoftware.eid.utils.EidPreconditions.tryToExecute; +import pl.wavesoftware.utils.stringify.configuration.Mode; +import pl.wavesoftware.utils.stringify.configuration.BeanFactory; /** - * @author Krzysztof Suszyński - * @since 2018-04-18 + * @author Krzysztof Suszynski + * @since 27.04.18 */ -public final class ToStringResolver { - private static final Iterable OBJECT_INSPECTORS = Arrays.asList( - new CharSequenceInspector(), - new PrimitiveInspector(), - new CharacterInspector(), - new JPALazyInspector(), - new MapInspector(), - new IterableInspector(), - new RecursionInspector() - ); - - private final Object target; - private final State state; - private final Function alternative; - - +public interface ToStringResolver { /** - * A default constructor + * Configures a mode in which resolver operates * - * @param target a target object to resolve + * @param mode a mode to set + * @return a self reference */ - public ToStringResolver(Object target) { - this(target, new StateImpl()); - } + ToStringResolver withMode(Mode mode); - private ToStringResolver(Object target, - State state) { - this.state = state; - this.target = target; - this.alternative = new ObjectInspectorImpl(); - } + /** + * Configures a predicate factory to be used to load implementation of predicates used + * + * @param beanFactory a predicate factory + * @return a self reference + */ + ToStringResolver withBeanFactory(BeanFactory beanFactory); /** * Resolves a {@link String} representation of given object. * * @return String representation */ - public CharSequence resolve() { - state.markIsInspected(target); - StringBuilder sb = new StringBuilder(); - sb.append('<'); - sb.append(target.getClass().getSimpleName()); - CharSequence props = propertiesForToString(); - if (props.length() != 0) { - sb.append(' '); - sb.append(props); - } - sb.append('>'); - return sb; - } - - private CharSequence propertiesForToString() { - Map props; - props = inspectTargetAsClass(target.getClass()); - StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : props.entrySet()) { - String fieldName = entry.getKey(); - CharSequence fieldStringValue = entry.getValue(); - sb.append(fieldName); - sb.append("="); - sb.append(fieldStringValue); - sb.append(", "); - } - if (sb.length() > 0) { - sb.deleteCharAt(sb.length() - 1); - sb.deleteCharAt(sb.length() - 1); - } - return sb; - } - - private Map inspectTargetAsClass(Class type) { - Class supertype = type.getSuperclass(); - Map props; - if (supertype == null || supertype.equals(Object.class)) { - props = new LinkedHashMap<>(); - } else { - props = inspectTargetAsClass(supertype); - } - inspectFields(type.getDeclaredFields(), props); - return props; - } - - private void inspectFields(Field[] fields, - Map properties) { - for (Field field : fields) { - boolean accessable = field.isAccessible(); - if (!accessable) { - field.setAccessible(true); - } - if (field.isAnnotationPresent(Inspect.class)) { - Inspect annot = field.getAnnotation(Inspect.class); - inspectAnnotatedField(properties, field, annot); - } - } - } - - private void inspectAnnotatedField(final Map properties, - final Field field, - final Inspect inspect) { - tryToExecute(new EidPreconditions.UnsafeProcedure() { - @Override - public void execute() throws Exception { - Object value = field.get(target); - if (value == null) { - if (inspect.showNull()) { - properties.put(field.getName(), null); - } - } else { - properties.put(field.getName(), inspectObject(value)); - } - } - }, "20130422:154938"); - } - - private CharSequence inspectObject(Object object) { - for (ObjectInspector inspector : OBJECT_INSPECTORS) { - if (inspector.consentTo(object, state)) { - return inspector.inspect(object, alternative); - } - } - ToStringResolver sub = new ToStringResolver(object, state); - return sub.resolve(); - } - - private final class ObjectInspectorImpl implements Function { - - @Override - public CharSequence apply(Object object) { - return inspectObject(object); - } - } - - private static final class StateImpl implements State { - private static final Object CONTAIN = new Object(); - - private final Map resolved; - - private StateImpl() { - this(new IdentityHashMap<>()); - } - - private StateImpl(Map resolved) { - this.resolved = resolved; - } - - @Override - public boolean wasInspected(Object object) { - return resolved.containsKey(object); - } - - @Override - public void markIsInspected(Object object) { - resolved.put(object, CONTAIN); - } - } + CharSequence resolve(); } diff --git a/src/main/java/pl/wavesoftware/utils/stringify/impl/ToStringResolverFactory.java b/src/main/java/pl/wavesoftware/utils/stringify/impl/ToStringResolverFactory.java new file mode 100644 index 0000000..9582aef --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/stringify/impl/ToStringResolverFactory.java @@ -0,0 +1,9 @@ +package pl.wavesoftware.utils.stringify.impl; + +/** + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +public interface ToStringResolverFactory { + ToStringResolver create(Object target); +} diff --git a/src/main/java/pl/wavesoftware/utils/stringify/impl/ToStringResolverFactoryImpl.java b/src/main/java/pl/wavesoftware/utils/stringify/impl/ToStringResolverFactoryImpl.java new file mode 100644 index 0000000..6a588a8 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/stringify/impl/ToStringResolverFactoryImpl.java @@ -0,0 +1,12 @@ +package pl.wavesoftware.utils.stringify.impl; + +/** + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +public final class ToStringResolverFactoryImpl implements ToStringResolverFactory { + @Override + public ToStringResolver create(Object target) { + return new ToStringResolverImpl(target); + } +} diff --git a/src/main/java/pl/wavesoftware/utils/stringify/impl/ToStringResolverImpl.java b/src/main/java/pl/wavesoftware/utils/stringify/impl/ToStringResolverImpl.java new file mode 100644 index 0000000..caca7f6 --- /dev/null +++ b/src/main/java/pl/wavesoftware/utils/stringify/impl/ToStringResolverImpl.java @@ -0,0 +1,201 @@ +package pl.wavesoftware.utils.stringify.impl; + +import pl.wavesoftware.eid.utils.EidPreconditions; +import pl.wavesoftware.utils.stringify.configuration.Mode; +import pl.wavesoftware.utils.stringify.configuration.BeanFactory; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import static pl.wavesoftware.eid.utils.EidPreconditions.tryToExecute; + +/** + * @author Krzysztof Suszyński + * @since 2018-04-18 + */ +final class ToStringResolverImpl implements ToStringResolver { + private static final Iterable OBJECT_INSPECTORS = Arrays.asList( + new CharSequenceInspector(), + new PrimitiveInspector(), + new CharacterInspector(), + new JPALazyInspector(), + new MapInspector(), + new IterableInspector(), + new RecursionInspector() + ); + + private final Object target; + private InspectingFieldFactory inspectingFieldFactory; + private BeanFactory beanFactory; + private final State state; + private final Function alternative; + + + /** + * A default constructor + * + * @param target a target object to resolve + */ + ToStringResolverImpl(Object target) { + this( + target, + new StateImpl(), + new ReflectionBeanFactory(), + new InspectingFieldFactory(Mode.DEFAULT_MODE) + ); + } + + private ToStringResolverImpl(Object target, + State state, + BeanFactory beanFactory, + InspectingFieldFactory inspectingFieldFactory) { + this.state = state; + this.target = target; + this.beanFactory = beanFactory; + this.inspectingFieldFactory = inspectingFieldFactory; + this.alternative = new ObjectInspectorImpl(); + } + + @Override + public ToStringResolver withMode(Mode mode) { + this.inspectingFieldFactory = new InspectingFieldFactory(mode); + return this; + } + + @Override + public ToStringResolver withBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + return this; + } + + @Override + public CharSequence resolve() { + state.markIsInspected(target); + StringBuilder sb = new StringBuilder(); + sb.append('<'); + sb.append(target.getClass().getSimpleName()); + CharSequence props = propertiesForToString(); + if (props.length() != 0) { + sb.append(' '); + sb.append(props); + } + sb.append('>'); + return sb; + } + + private CharSequence propertiesForToString() { + Map props; + props = inspectTargetAsClass(target.getClass()); + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : props.entrySet()) { + String fieldName = entry.getKey(); + CharSequence fieldStringValue = entry.getValue(); + sb.append(fieldName); + sb.append("="); + sb.append(fieldStringValue); + sb.append(", "); + } + if (sb.length() > 0) { + sb.deleteCharAt(sb.length() - 1); + sb.deleteCharAt(sb.length() - 1); + } + return sb; + } + + private Map inspectTargetAsClass(Class type) { + Class supertype = type.getSuperclass(); + Map props; + if (supertype == null || supertype.equals(Object.class)) { + props = new LinkedHashMap<>(); + } else { + props = inspectTargetAsClass(supertype); + } + inspectFields(type.getDeclaredFields(), props); + return props; + } + + private void inspectFields(Field[] fields, + Map properties) { + for (Field field : fields) { + InspectingField inspectingField = inspectingFieldFactory.create(field, beanFactory); + if (inspectingField.shouldInspect()) { + inspectAnnotatedField(properties, field, inspectingField); + } + } + } + + private void inspectAnnotatedField(final Map properties, + final Field field, + final InspectingField inspectingField) { + tryToExecute(new EidPreconditions.UnsafeProcedure() { + @Override + public void execute() throws IllegalAccessException { + ensureAccessible(field); + Object value = field.get(target); + if (value == null) { + if (inspectingField.showNull()) { + properties.put(field.getName(), null); + } + } else { + properties.put(field.getName(), inspectObject(value)); + } + } + }, "20130422:154938"); + } + + private void ensureAccessible(Field field) { + if (!field.isAccessible()) { + field.setAccessible(true); + } + } + + private CharSequence inspectObject(Object object) { + for (ObjectInspector inspector : OBJECT_INSPECTORS) { + if (inspector.consentTo(object, state)) { + return inspector.inspect(object, alternative); + } + } + ToStringResolverImpl sub = new ToStringResolverImpl( + object, + state, + beanFactory, + inspectingFieldFactory + ); + return sub.resolve(); + } + + private final class ObjectInspectorImpl implements Function { + + @Override + public CharSequence apply(Object object) { + return inspectObject(object); + } + } + + private static final class StateImpl implements State { + private static final Object CONTAIN = new Object(); + + private final Map resolved; + + private StateImpl() { + this(new IdentityHashMap<>()); + } + + private StateImpl(Map resolved) { + this.resolved = resolved; + } + + @Override + public boolean wasInspected(Object object) { + return resolved.containsKey(object); + } + + @Override + public void markIsInspected(Object object) { + resolved.put(object, CONTAIN); + } + } +} diff --git a/src/test/java/pl/wavesoftware/utils/stringify/Account.java b/src/test/java/pl/wavesoftware/utils/stringify/Account.java new file mode 100644 index 0000000..b261b52 --- /dev/null +++ b/src/test/java/pl/wavesoftware/utils/stringify/Account.java @@ -0,0 +1,11 @@ +package pl.wavesoftware.utils.stringify; + +import java.sql.Blob; + +/** + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +class Account { + private Blob heavy; +} diff --git a/src/test/java/pl/wavesoftware/utils/stringify/Earth.java b/src/test/java/pl/wavesoftware/utils/stringify/Earth.java index a65b801..71b80cd 100644 --- a/src/test/java/pl/wavesoftware/utils/stringify/Earth.java +++ b/src/test/java/pl/wavesoftware/utils/stringify/Earth.java @@ -2,7 +2,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; -import pl.wavesoftware.utils.stringify.annotation.Inspect; +import pl.wavesoftware.utils.stringify.configuration.Inspect; /** * @author Krzysztof Suszyński @@ -18,6 +18,7 @@ final class Earth extends Planet { @Inspect private char type; + Earth() { super(true, "Earth"); } diff --git a/src/test/java/pl/wavesoftware/utils/stringify/IsInDevelopment.java b/src/test/java/pl/wavesoftware/utils/stringify/IsInDevelopment.java new file mode 100644 index 0000000..2bd9e2e --- /dev/null +++ b/src/test/java/pl/wavesoftware/utils/stringify/IsInDevelopment.java @@ -0,0 +1,12 @@ +package pl.wavesoftware.utils.stringify; + +import pl.wavesoftware.utils.stringify.configuration.Predicate; + +import java.lang.reflect.Field; + +/** + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +interface IsInDevelopment extends Predicate { +} diff --git a/src/test/java/pl/wavesoftware/utils/stringify/Moon.java b/src/test/java/pl/wavesoftware/utils/stringify/Moon.java index d8fd74a..fcb4e19 100644 --- a/src/test/java/pl/wavesoftware/utils/stringify/Moon.java +++ b/src/test/java/pl/wavesoftware/utils/stringify/Moon.java @@ -2,7 +2,8 @@ import lombok.Data; import lombok.EqualsAndHashCode; -import pl.wavesoftware.utils.stringify.annotation.Inspect; +import lombok.ToString; +import pl.wavesoftware.utils.stringify.configuration.Inspect; import java.util.LinkedHashMap; import java.util.List; @@ -13,12 +14,15 @@ * @since 2018-04-18 */ @Data +@ToString(exclude = "nullinside") @EqualsAndHashCode(callSuper = true) final class Moon extends Planet { @Inspect private Phase phase; @Inspect private Map> visits = new LinkedHashMap<>(); + @Inspect + private Object nullinside; Moon(Phase phase) { super(null, "Moon"); diff --git a/src/test/java/pl/wavesoftware/utils/stringify/ObjectStringifierTest.java b/src/test/java/pl/wavesoftware/utils/stringify/ObjectStringifierTest.java index f299efe..852fe1e 100644 --- a/src/test/java/pl/wavesoftware/utils/stringify/ObjectStringifierTest.java +++ b/src/test/java/pl/wavesoftware/utils/stringify/ObjectStringifierTest.java @@ -1,9 +1,20 @@ package pl.wavesoftware.utils.stringify; +import lombok.RequiredArgsConstructor; import org.junit.Test; +import pl.wavesoftware.utils.stringify.configuration.AlwaysTruePredicate; +import pl.wavesoftware.utils.stringify.configuration.Mode; +import pl.wavesoftware.utils.stringify.configuration.Predicate; +import pl.wavesoftware.utils.stringify.configuration.BeanFactory; + +import java.lang.reflect.Field; +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.Map; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; /** @@ -15,10 +26,11 @@ public class ObjectStringifierTest { private final TestRepository testRepository = new TestRepository(); @Test - public void testToString() { + public void testInQuietMode() { // given Planet planet = testRepository.createTestPlanet(); ObjectStringifier stringifier = new ObjectStringifier(planet); + stringifier.setMode(Mode.QUIET); // when String repr = stringifier.toString(); @@ -31,7 +43,23 @@ public void testToString() { } @Test - public void testToStringByLombok() { + public void testInPromiscuousMode() { + // given + Planet planet = testRepository.createTestPlanet(); + ObjectStringifier stringifier = new ObjectStringifier(planet); + + // when + String repr = stringifier.toString(); + + // then + assertEquals(", moon=, dayOfYear=14, type='A'>", repr); + } + + @Test + public void testByLombok() { // given Planet planet = testRepository.createTestPlanet(); @@ -43,4 +71,103 @@ public void testToStringByLombok() { " 1971=[Apollo 14, Apollo 15], 1972=[Apollo 16, Apollo 17]}), dayOfYear=14, type=A)", repr); } + @Test + public void testWithCustomPredicate() throws NoSuchFieldException { + // given + SimpleUser user = testRepository.createTestSimpleUser(); + ObjectStringifier stringifier = new ObjectStringifier(user); + + // when + ProductionEnvironment.setProduction(true); + String productionResult = stringifier.toString(); + ProductionEnvironment.setProduction(false); + String developmentResult = stringifier.toString(); + + // then + assertEquals("", productionResult); + assertEquals("", + developmentResult); + assertTrue(new AlwaysTruePredicate().test( + this.getClass().getDeclaredField("testRepository") + )); + } + + @Test + public void testWithCustomPredicateWithPredicateFactory() { + // given + User user = testRepository.createTestUser(); + IsInDevelopment isDevelopment = new IsInDevelopmentImpl(true); + IsInDevelopment isProduction = new IsInDevelopmentImpl(false); + BeanFactory productionBeanFactory = getBeanFactory(isProduction); + BeanFactory developmentBeanFactory = getBeanFactory(isDevelopment); + ObjectStringifier stringifier = new ObjectStringifier(user); + + // when + stringifier.setBeanFactory(productionBeanFactory); + String productionResult = stringifier.toString(); + stringifier.setBeanFactory(developmentBeanFactory); + String developmentResult = stringifier.toString(); + stringifier.setMode(Mode.QUIET); + stringifier.setBeanFactory(developmentBeanFactory); + String developmentQuietResult = stringifier.toString(); + + // then + assertEquals("", productionResult); + assertEquals("", developmentResult); + assertEquals("", developmentQuietResult); + } + + @Test + public void testOnPerson() { + // given + Person person = testRepository.createPerson(); + ObjectStringifier stringifier = new ObjectStringifier(person); + IsInDevelopment isProduction = new IsInDevelopmentImpl(false); + BeanFactory productionBeanFactory = getBeanFactory(isProduction); + stringifier.setBeanFactory(productionBeanFactory); + + // when + String productionResult = stringifier.toString(); + + // then + assertEquals(", childs=[], account=⁂Lazy>", productionResult); + } + + private StaticBeanFactory getBeanFactory(IsInDevelopment isInDevelopmentFalse) { + return new StaticBeanFactory( + new AbstractMap.SimpleImmutableEntry, Predicate>( + IsInDevelopment.class, isInDevelopmentFalse + ) + ); + } + + @RequiredArgsConstructor + private static final class IsInDevelopmentImpl implements IsInDevelopment { + private final boolean development; + + @Override + public boolean test(Field field) { + return development; + } + } + + private static final class StaticBeanFactory implements BeanFactory { + private final Map, Object> instances = new HashMap<>(); + + @SafeVarargs + StaticBeanFactory(Map.Entry, Predicate>... entries) { + for (Map.Entry, Predicate> entry : entries) { + instances.put(entry.getKey(), entry.getValue()); + } + } + + @Override + @SuppressWarnings("unchecked") + public T create(final Class contractClass) { + Object bean = instances.get(contractClass); + return (T) bean; + } + } + } diff --git a/src/test/java/pl/wavesoftware/utils/stringify/Person.java b/src/test/java/pl/wavesoftware/utils/stringify/Person.java new file mode 100644 index 0000000..cdb6a67 --- /dev/null +++ b/src/test/java/pl/wavesoftware/utils/stringify/Person.java @@ -0,0 +1,25 @@ +package pl.wavesoftware.utils.stringify; + +import lombok.Setter; +import pl.wavesoftware.utils.stringify.configuration.DisplayNull; +import pl.wavesoftware.utils.stringify.configuration.DoNotInspect; +import pl.wavesoftware.utils.stringify.configuration.Inspect; + +import java.util.List; + +/** + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +@Setter +class Person { + private int id; + @DisplayNull + private Person parent; + private List childs; + private Account account; + @Inspect(conditionally = IsInDevelopment.class) + private String password; + @DoNotInspect + private String ignored; +} diff --git a/src/test/java/pl/wavesoftware/utils/stringify/Planet.java b/src/test/java/pl/wavesoftware/utils/stringify/Planet.java index 0e80ac1..efe21c0 100644 --- a/src/test/java/pl/wavesoftware/utils/stringify/Planet.java +++ b/src/test/java/pl/wavesoftware/utils/stringify/Planet.java @@ -1,7 +1,9 @@ package pl.wavesoftware.utils.stringify; import lombok.Data; -import pl.wavesoftware.utils.stringify.annotation.Inspect; +import pl.wavesoftware.utils.stringify.configuration.DisplayNull; +import pl.wavesoftware.utils.stringify.configuration.DoNotInspect; +import pl.wavesoftware.utils.stringify.configuration.Inspect; /** * @author Krzysztof Suszyński @@ -11,10 +13,13 @@ abstract class Planet { @Inspect private String name; - @Inspect(showNull = true) + @Inspect + @DisplayNull private Boolean rocky; @Inspect private PlanetSystem planetSystem; + @DoNotInspect + private String ignored; Planet(Boolean rocky, String name) { this.rocky = rocky; diff --git a/src/test/java/pl/wavesoftware/utils/stringify/PlanetSystem.java b/src/test/java/pl/wavesoftware/utils/stringify/PlanetSystem.java index fbc45f8..ede0484 100644 --- a/src/test/java/pl/wavesoftware/utils/stringify/PlanetSystem.java +++ b/src/test/java/pl/wavesoftware/utils/stringify/PlanetSystem.java @@ -1,7 +1,7 @@ package pl.wavesoftware.utils.stringify; import lombok.Data; -import pl.wavesoftware.utils.stringify.annotation.Inspect; +import pl.wavesoftware.utils.stringify.configuration.Inspect; import java.util.ArrayList; import java.util.List; diff --git a/src/test/java/pl/wavesoftware/utils/stringify/ProductionEnvironment.java b/src/test/java/pl/wavesoftware/utils/stringify/ProductionEnvironment.java new file mode 100644 index 0000000..dfb5346 --- /dev/null +++ b/src/test/java/pl/wavesoftware/utils/stringify/ProductionEnvironment.java @@ -0,0 +1,22 @@ +package pl.wavesoftware.utils.stringify; + +import pl.wavesoftware.utils.stringify.configuration.Predicate; + +import java.lang.reflect.Field; + +/** + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +public final class ProductionEnvironment implements Predicate { + private static boolean production = true; + + static void setProduction(boolean setting) { + production = setting; + } + + @Override + public boolean test(Field field) { + return production; + } +} diff --git a/src/test/java/pl/wavesoftware/utils/stringify/SimpleUser.java b/src/test/java/pl/wavesoftware/utils/stringify/SimpleUser.java new file mode 100644 index 0000000..82ba440 --- /dev/null +++ b/src/test/java/pl/wavesoftware/utils/stringify/SimpleUser.java @@ -0,0 +1,19 @@ +package pl.wavesoftware.utils.stringify; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import pl.wavesoftware.utils.stringify.configuration.DoNotInspect; + +/** + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +@Getter +@RequiredArgsConstructor +final class SimpleUser { + private final String login; + @DoNotInspect(conditionally = ProductionEnvironment.class) + private final String password; + @DoNotInspect(conditionally = ProductionEnvironment.class) + private final String phoneNumber; +} diff --git a/src/test/java/pl/wavesoftware/utils/stringify/TestRepository.java b/src/test/java/pl/wavesoftware/utils/stringify/TestRepository.java index a88798a..19a0291 100644 --- a/src/test/java/pl/wavesoftware/utils/stringify/TestRepository.java +++ b/src/test/java/pl/wavesoftware/utils/stringify/TestRepository.java @@ -1,8 +1,13 @@ package pl.wavesoftware.utils.stringify; import org.hibernate.collection.internal.PersistentList; +import org.hibernate.proxy.AbstractLazyInitializer; +import org.hibernate.proxy.HibernateProxy; +import org.hibernate.proxy.LazyInitializer; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; /** * @author Krzysztof Suszynski @@ -17,12 +22,60 @@ Planet createTestPlanet() { moon.getVisits().put("1969", Arrays.asList("Apollo 11", "Apollo 12")); moon.getVisits().put("1971", Arrays.asList("Apollo 14", "Apollo 15")); moon.getVisits().put("1972", Arrays.asList("Apollo 16", "Apollo 17")); + moon.setIgnored("Ignored value"); Earth earth = new Earth(); earth.setDayOfYear(14); earth.setType('A'); earth.setMoon(moon); + earth.setIgnored("This should not be presented"); earth.setPlanetSystem(earthPlanetSystem); earthPlanetSystem.setPlanets(new PersistentList()); return earth; } + + User createTestUser() { + return new User("jdoe", "1qaz2wsx!@"); + } + + SimpleUser createTestSimpleUser() { + return new SimpleUser("llohan", "1234567890", "555-123-445"); + } + + Person createPerson() { + Account a1 = new LazyAccount(); + Account a2 = new LazyAccount(); + Person child = new Person(); + Person parent = new Person(); + parent.setId(16); + parent.setChilds(Collections.singletonList(child)); + parent.setAccount(a2); + parent.setPassword("!@#$4321qwer"); + parent.setIgnored("Ignore it!"); + child.setId(15); + child.setParent(parent); + child.setChilds(new ArrayList()); + child.setAccount(a1); + child.setPassword("rewq1234$#@!"); + child.setIgnored("Dump this"); + return child; + } + + private static final class LazyAccount extends Account implements HibernateProxy { + private static final long serialVersionUID = 20180427194122L; + + @Override + public Object writeReplace() { + return hashCode(); + } + + @Override + public LazyInitializer getHibernateLazyInitializer() { + return new AbstractLazyInitializer() { + @Override + public Class getPersistentClass() { + return LazyAccount.class; + } + }; + } + } } diff --git a/src/test/java/pl/wavesoftware/utils/stringify/User.java b/src/test/java/pl/wavesoftware/utils/stringify/User.java new file mode 100644 index 0000000..594745f --- /dev/null +++ b/src/test/java/pl/wavesoftware/utils/stringify/User.java @@ -0,0 +1,17 @@ +package pl.wavesoftware.utils.stringify; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import pl.wavesoftware.utils.stringify.configuration.Inspect; + +/** + * @author Krzysztof Suszynski + * @since 27.04.18 + */ +@Getter +@RequiredArgsConstructor +final class User { + private final String login; + @Inspect(conditionally = IsInDevelopment.class) + private final String password; +} From 9e93677784fad8821ea26b93f1ae1ed41a958b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Suszy=C5=84ski?= Date: Sat, 28 Apr 2018 19:22:22 +0200 Subject: [PATCH 2/2] Change performance threshold for Java 9 --- .../wavesoftware/utils/stringify/ObjectStringifierIT.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/java/pl/wavesoftware/utils/stringify/ObjectStringifierIT.java b/src/test/java/pl/wavesoftware/utils/stringify/ObjectStringifierIT.java index 0127f79..8cdc5b2 100644 --- a/src/test/java/pl/wavesoftware/utils/stringify/ObjectStringifierIT.java +++ b/src/test/java/pl/wavesoftware/utils/stringify/ObjectStringifierIT.java @@ -97,8 +97,11 @@ public void stringifier(Blackhole bh) { private static double getSpeedThreshold() { double jreVersion = Double.parseDouble(System.getProperty("java.specification.version")); - if (jreVersion > 1.7d) { - // 8% performance of static lombok code for Java 8+ + if (jreVersion >= 9d) { + // 5% performance of static lombok code for Java 9+ + return 0.05d; + } else if (jreVersion >= 1.8d) { + // 8% performance of static lombok code for Java 8 return 0.08d; } else { // 1% performance of static lombok code for Java 7