Skip to content

Commit

Permalink
✨ New constructor rule
Browse files Browse the repository at this point in the history
  • Loading branch information
lengors committed Sep 2, 2024
1 parent ef85427 commit 01963ee
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package io.github.lengors.js2pets.rules;

import org.apache.commons.collections4.IteratorUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.jsonschema2pojo.Schema;
import org.jsonschema2pojo.rules.Rule;
import org.jsonschema2pojo.rules.RuleFactory;

import com.fasterxml.jackson.databind.JsonNode;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JMethod;

import io.github.lengors.js2pets.annotators.EnhancedAnnotator;
import io.github.lengors.js2pets.rules.exceptions.ConfigurationPropertyMissingException;
import lombok.AllArgsConstructor;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

/**
* Constructor rule wrapper that removes the no-args constructor if the respective flag is enabled. This rule also
* notifies the annotator if it supports constructor callbacks.
*
* This class extends the functionality provided by the jsonschema2pojo library by adding a customizable rule for
* generating or omitting no-argument constructors based on configuration settings.
*
* @author lengors
*/
@AllArgsConstructor
public class ConstructorRule implements Rule<JDefinedClass, JDefinedClass> {
/**
* Rule factory from where we get generation configuration among other utilities.
*/
private final RuleFactory ruleFactory;

/**
* Flag determining whether the no-args constructor should be included in the resulting set of constructors or not.
* Leaving the flag null will lead the rule to try to infer it from the plugin's configuration.
*/
private final @Nullable Boolean includeNoArgsConstructor;

/**
* The constructor rule that must be obtained from the super rule's factory.
*/
private final Rule<JDefinedClass, JDefinedClass> superConstructorRule;

/**
* Applies this rule to the given {@link JDefinedClass}, potentially removing the no-args constructor and notifying
* the annotator if applicable.
*
* @param nodeName The name of the JSON node being processed.
* @param node The JSON node to which the rule is being applied.
* @param parent The parent JSON node, or null if there isn't one.
* @param type The Java class that is being generated from the JSON
* schema.
* @param currentSchema The current schema being processed.
* @return The {@link JDefinedClass} after applying the rule.
*/
@Override
public JDefinedClass apply(
final String nodeName,
final JsonNode node,
final JsonNode parent,
final JDefinedClass type,
final Schema currentSchema) {
final var clazz = superConstructorRule.apply(nodeName, node, parent, type, currentSchema);

if (!isIncludeNoArgsConstructor()) {
removeConstructors(clazz, ruleFactory);
}

if (ruleFactory.getAnnotator() instanceof EnhancedAnnotator annotator) {
IteratorUtils.forEach(clazz.constructors(), annotator::constructor);
}

return clazz;
}

private boolean isIncludeNoArgsConstructor() {
return Optional
.ofNullable(includeNoArgsConstructor)
.orElseGet(() -> {
final var generationConfig = ruleFactory.getGenerationConfig();
final var method = MethodUtils.getAccessibleMethod(generationConfig.getClass(), "getPluginContext");
if (method == null) {
throw new ConfigurationPropertyMissingException(INCLUDE_NO_ARGS_CONSTRUCTOR_KEY);
}

final var pluginContext = invokeMethod(method, generationConfig, Map.class)
.orElseGet(Collections::emptyMap);
final var optPlugin = Optional
.ofNullable(pluginContext.get("pluginDescriptor"))
.filter(PluginDescriptor.class::isInstance)
.map(PluginDescriptor.class::cast)
.map(PluginDescriptor::getPlugin);

final var executionsCount = optPlugin
.map(Plugin::getExecutions)
.map(List::size)
.orElse(0);

if (executionsCount > 1) {
throw new ConfigurationPropertyMissingException(INCLUDE_NO_ARGS_CONSTRUCTOR_KEY);
}

return optPlugin
.map(Plugin::getConfiguration)
.filter(Xpp3Dom.class::isInstance)
.map(Xpp3Dom.class::cast)
.map(configuration -> configuration.getChild(INCLUDE_NO_ARGS_CONSTRUCTOR_KEY))
.map(Xpp3Dom::getValue)
.map(Boolean::parseBoolean)
.orElse(DEFAULT_INCLUDE_NO_ARGS_CONSTRUCTOR);
});
}

private static <T> Optional<T> invokeMethod(
final Method method,
final Object target,
final Class<? extends T> targetType) {
try {
return Optional
.ofNullable(method.invoke(target))
.filter(targetType::isInstance)
.map(targetType::cast);
} catch (final IllegalAccessException | InvocationTargetException exception) {
return Optional.empty();
}
}

private static @Nullable Object readFieldValue(
final JDefinedClass target,
final Field field,
final RuleFactory ruleFactory) {
if (Modifier.isStatic(field.getModifiers())) {
return null;
}

try {
return FieldUtils.readField(field, target, true);
} catch (final IllegalAccessException exception) {
ruleFactory
.getLogger()
.warn(String.format("Could not access JDefinedClass{} field Field{name=%s}", field.getName()), exception);
}
return null;
}

private static void removeConstructors(final JDefinedClass clazz, final RuleFactory ruleFactory) {
final var knownConstructors = IteratorUtils.toList(clazz.constructors());
for (final var field : FieldUtils.getAllFields(clazz.getClass())) {
if (readFieldValue(clazz, field, ruleFactory) instanceof Collection<?> collection) {
final var noArgsConstructors = new ArrayList<JMethod>();
for (final var element : collection) {
if (element instanceof JMethod method
&& method.listParams().length == 0
&& knownConstructors.contains(method)) {
noArgsConstructors.add(method);
}
}
noArgsConstructors.forEach(method -> remove(collection, method));
}
}
}

private static boolean remove(final Collection<?> collection, final Object value) {
try {
return collection.remove(value);
} catch (final UnsupportedOperationException exception) {
return false;
}
}

/**
* The key used to retrieve the "includeNoArgsConstructor" configuration property.
*/
private static final String INCLUDE_NO_ARGS_CONSTRUCTOR_KEY = "includeNoArgsConstructor";

/**
* The default value for whether the no-args constructor should be included.
*/
private static final boolean DEFAULT_INCLUDE_NO_ARGS_CONSTRUCTOR = true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.github.lengors.js2pets.rules.exceptions;

import lombok.Getter;

/**
* Exception thrown when a required configuration property is missing and its value cannot be inferred.
*
* This exception is a specific type of {@link NullPointerException} that includes the name of the missing configuration
* property for easier debugging and context understanding.
*
* @author lengors
*/
public class ConfigurationPropertyMissingException extends NullPointerException {
/**
* The name of the configuration property that is missing.
*/
@Getter
private final String configurationPropertyName;

/**
* Constructs a new {@code ConfigurationPropertyMissingException} with the specified property name.
*
* @param configurationPropertyName The name of the missing configuration property.
*/
public ConfigurationPropertyMissingException(final String configurationPropertyName) {
super(String.format(
"Configuration property {name=%s} missing and value could not be inferred from context",
configurationPropertyName));
this.configurationPropertyName = configurationPropertyName;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* This package contains custom exceptions used in the JS2Pets library.
*
* The exceptions in this package are designed to handle specific error cases related to configuration and rule
* processing within the framework, making it easier to diagnose and respond to issues during runtime.
*
* @author lengors
*/
package io.github.lengors.js2pets.rules.exceptions;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* This package contains the rules that extend the functionality of the JS2Pets library.
*
* These rules are used to customize the behavior of jsonschema2pojo, specifically in handling constructor generation
* and related tasks within the context of JSON Schema to POJO conversion.
*
* @author lengors
*/
package io.github.lengors.js2pets.rules;

0 comments on commit 01963ee

Please sign in to comment.