Skip to content

Commit

Permalink
✨ Add NullabilityAnnotator and deprecate CheckerableAnnotator and…
Browse files Browse the repository at this point in the history
… related classes
  • Loading branch information
lengors committed Nov 24, 2024
1 parent 3c9da72 commit 3622c1f
Show file tree
Hide file tree
Showing 11 changed files with 499 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package io.github.lengors.js2pets.annotators;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.jsonschema2pojo.GenerationConfig;

/**
* Annotation utilities.
*
* @author lengors
*/
public final class AnnotationUtils {
/**
* Checkerframework nullable annotation.
*/
public static final List<Class<? extends Annotation>> CHECKERFRAMEWORK_NULLABLE_ANNOTATION = List.of(Nullable.class);

/**
* Checkerframework nullability annotations.
*/
public static final Set<String> CHECKERFRAMEWORK_NULLABILITY_ANNOTATIONS = Stream
.of(NonNull.class, Nullable.class)
.map(Class::getName)
.collect(Collectors.toUnmodifiableSet());

/**
* Non-nullable annotations.
*/
public static final List<Class<? extends Annotation>> NON_NULLABLE_ANNOTATIONS = List.of(
jakarta.validation.constraints.NotNull.class,
javax.validation.constraints.NotNull.class,
jakarta.annotation.Nonnull.class,
javax.annotation.Nonnull.class,
NonNull.class);

/**
* Nullable annotations.
*/
public static final List<Class<? extends Annotation>> NULLABLE_ANNOTATIONS = List.of(
jakarta.annotation.Nullable.class,
javax.annotation.Nullable.class,
Nullable.class);

private AnnotationUtils() {
throw new UnsupportedOperationException();
}

/**
* Gets the non-nullable annotations based on generation configuration.
*
* @param generationConfig The generation configuration.
* @return The non-nullable annotation classes.
*/
public static List<Class<? extends Annotation>> getNonNullableAnnotations(final GenerationConfig generationConfig) {
final var annotations = new ArrayList<Class<? extends Annotation>>();
if (generationConfig.isIncludeJsr303Annotations()) {
if (generationConfig.isUseJakartaValidation()) {
annotations.add(jakarta.validation.constraints.NotNull.class);
} else {
annotations.add(javax.validation.constraints.NotNull.class);
}
}

if (generationConfig.isIncludeJsr305Annotations()) {
if (generationConfig.isUseJakartaValidation()) {
annotations.add(jakarta.annotation.Nonnull.class);
} else {
annotations.add(javax.annotation.Nonnull.class);
}
}

annotations.add(NonNull.class);

return Collections.unmodifiableList(annotations);
}

/**
* Gets the nullable annotations based on generation configuration.
*
* @param generationConfig The generation configuration.
* @return The nullable annotation classes.
*/
public static List<Class<? extends Annotation>> getNullableAnnotations(final GenerationConfig generationConfig) {
final var annotations = new ArrayList<Class<? extends Annotation>>();
if (generationConfig.isIncludeJsr305Annotations()) {
if (generationConfig.isUseJakartaValidation()) {
annotations.add(jakarta.annotation.Nullable.class);
} else {
annotations.add(javax.annotation.Nullable.class);
}
}

annotations.add(Nullable.class);

return Collections.unmodifiableList(annotations);
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,24 @@
package io.github.lengors.js2pets.annotators;

import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.SequencedCollection;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.apache.commons.collections4.IteratorUtils;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.jsonschema2pojo.NoopAnnotator;

import com.sun.codemodel.JAnnotationUse;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JType;

import io.github.lengors.js2pets.codemodel.CodeModelUtils;

/**
* An annotator that makes the generated classes compatible with the Checkerframework.
*
* @author lengors
* @deprecated Use {@link NullabilityAnnotator} instead with an instance of
* {@link io.github.lengors.js2pets.factories.EnhancedRuleFactory}.
*/
@Deprecated(since = "1.2.0", forRemoval = true)
public class CheckerableAnnotator extends NoopAnnotator implements EnhancedAnnotator {

/**
Expand All @@ -42,57 +37,15 @@ public void type(final JType type) {
.fields()
.values()
.stream()
.filter(field -> field
.annotations()
.stream()
.map(JAnnotationUse::getAnnotationClass)
.map(JClass::fullName)
.anyMatch(Nullable.class.getName()::equals))
.filter(field -> CodeModelUtils.containsAnnotation(field, Nullable.class))
.map(JFieldVar::name)
.collect(Collectors.toUnmodifiableSet());

final var classList = classes(clazz)
.toList();

Stream
.concat(classList
.stream()
.flatMap(classFromSet -> classFromSet
.methods()
.stream()),
classList
.stream()
.flatMap(classFromSet -> stream(classFromSet.constructors())))
.map(JMethod::params)
.flatMap(List::stream)
.filter(param -> nullableFields.contains(param.name()))
.forEach(param -> param.annotate(Nullable.class));

final var objectRef = clazz
.owner()
._ref(Object.class);

Optional
.ofNullable(clazz.getMethod("equals", new JType[] {
objectRef
}))
.map(JMethod::params)
.map(SequencedCollection::getFirst)
.ifPresent(parameter -> parameter.annotate(Nullable.class));
}

private static <T> Stream<T> stream(final Iterator<T> iterator) {
return stream(IteratorUtils.asIterable(iterator));
}

private static <T> Stream<T> stream(final Iterable<T> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
}

private static Stream<JDefinedClass> classes(final JDefinedClass definedClass) {
return Stream.concat(
Stream.of(definedClass),
stream(definedClass.classes())
.flatMap(CheckerableAnnotator::classes));
final var classStructure = CodeModelUtils.listClassStructure(clazz);
CodeModelUtils.annotateInvokablesParameters(
CodeModelUtils.streamInvokables(classStructure),
nullableFields,
AnnotationUtils.CHECKERFRAMEWORK_NULLABLE_ANNOTATION);
CodeModelUtils.annotateEqualsMethod(clazz);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package io.github.lengors.js2pets.annotators;

import java.util.Comparator;

import org.jsonschema2pojo.Annotator;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JType;

import io.github.lengors.js2pets.streams.StreamUtils;

/**
* Enhanced annotator that extends the base functionality of jsonschema2pojo's allowing respective implementations to
* listen to constructor generation or type generation and annotate them.
Expand All @@ -18,7 +25,7 @@ public interface EnhancedAnnotator extends Annotator {
*
* @param constructor The generated constructor.
*/
default void constructor(JMethod constructor) {
default void constructor(final JMethod constructor) {

}

Expand All @@ -27,7 +34,23 @@ default void constructor(JMethod constructor) {
*
* @param type The generated type.
*/
default void type(JType type) {
default void type(final JType type) {
if (!(type instanceof JDefinedClass clazz)) {
return;
}

StreamUtils
.stream(clazz.constructors())
.max(Comparator.comparing(constructor -> constructor
.params()
.size()))
.ifPresent(constructor -> {
constructor.annotate(JsonCreator.class);
for (final var parameter : constructor.params()) {
parameter
.annotate(JsonProperty.class)
.param("value", parameter.name());
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package io.github.lengors.js2pets.annotators;

import java.util.Collections;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.jsonschema2pojo.AbstractAnnotator;
import org.jsonschema2pojo.GenerationConfig;

import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JType;

import io.github.lengors.js2pets.codemodel.CodeModelUtils;

/**
* An annotator that makes the generated classes emitted with proper nullable and non-nullable annotations.
*
* @author lengors
*/
public class NullabilityAnnotator extends AbstractAnnotator implements EnhancedAnnotator {
/**
* Getter method prefix.
*/
private static final String GETTER_PREFIX = "get";

/**
* Instantiates annotator with generation configuration.
*
* @param generationConfig The injected generation configuration.
*/
public NullabilityAnnotator(final GenerationConfig generationConfig) {
super(generationConfig);
}

/**
* Annotator callback that annotates equals methods' parameter with proper nullable types as well as any properties,
* return types and parameters for constructors and setters for the respective fields.
*
* @param type The generated class to apply the annotation to.
*/
@Override
public void type(final JType type) {
EnhancedAnnotator.super.type(type);

if (!(type instanceof JDefinedClass clazz)) {
return;
}

final var nullableAnnotations = AnnotationUtils.getNullableAnnotations(getGenerationConfig());
final var nonNullableAnnotations = AnnotationUtils.getNonNullableAnnotations(getGenerationConfig());

final var classStructure = CodeModelUtils.listClassStructure(clazz);
final var invokables = CodeModelUtils.listInvokables(classStructure);

final var fieldsByNullabilityType = clazz
.fields()
.values()
.stream()
.collect(Collectors.groupingBy(field -> AnnotationUtils.NON_NULLABLE_ANNOTATIONS
.stream()
.anyMatch(annotation -> CodeModelUtils.containsAnnotation(field, annotation))
? NullabilityType.NON_NULLABLE
: NullabilityType.NULLABLE));

for (final var nullabilityType : NullabilityType.values()) {
final var annotations = switch (nullabilityType) {
case NullabilityType.NON_NULLABLE -> nonNullableAnnotations;
case NullabilityType.NULLABLE -> nullableAnnotations;
};
final var fields = fieldsByNullabilityType.getOrDefault(nullabilityType, Collections.emptyList());
final var fieldNames = fields
.stream()
.map(field -> {
CodeModelUtils.safeAnnotate(field, annotations);
return field;
})
.map(JFieldVar::name)
.collect(Collectors.toUnmodifiableSet());

CodeModelUtils.annotateInvokablesParameters(invokables.stream(), fieldNames, annotations);

final var capitalizedFieldNames = fieldNames
.stream()
.map(StringUtils::capitalize)
.collect(Collectors.toUnmodifiableSet());
invokables
.stream()
.filter(method -> {
if (!method.params().isEmpty()) {
return false;
}

final var methodName = method.name();
if (!methodName.startsWith(GETTER_PREFIX)) {
return false;
}

return capitalizedFieldNames.contains(StringUtils.capitalize(methodName.replaceFirst(GETTER_PREFIX, "")));
})
.forEach(method -> CodeModelUtils.safeAnnotate(method, annotations));
}

CodeModelUtils.annotateEqualsMethod(clazz);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.github.lengors.js2pets.annotators;

/**
* Types of nullability.
*
* @author lengors
*/
public enum NullabilityType {
/**
* Nullable type.
*/
NULLABLE,

/**
* Nullable type.
*/
NON_NULLABLE;
}
Loading

0 comments on commit 3622c1f

Please sign in to comment.