org.simplejavamail.internal.clisupport.BuilderApiToPicocliCommandsMapper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cli-module Show documentation
Show all versions of cli-module Show documentation
Simple API, Complex Emails. Now with CLI support
/*
* Copyright © 2009 Benny Bottema ([email protected])
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.simplejavamail.internal.clisupport;
import jakarta.activation.DataSource;
import jakarta.mail.internet.MimeMessage;
import lombok.val;
import org.bbottema.javareflection.BeanUtils;
import org.bbottema.javareflection.BeanUtils.Visibility;
import org.bbottema.javareflection.ClassUtils;
import org.bbottema.javareflection.MethodUtils;
import org.bbottema.javareflection.model.LookupMode;
import org.bbottema.javareflection.model.MethodModifier;
import org.bbottema.javareflection.valueconverter.ValueConversionHelper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.simplejavamail.api.email.CalendarMethod;
import org.simplejavamail.api.email.ContentTransferEncoding;
import org.simplejavamail.api.internal.clisupport.model.Cli;
import org.simplejavamail.api.internal.clisupport.model.CliDeclaredOptionSpec;
import org.simplejavamail.api.internal.clisupport.model.CliDeclaredOptionValue;
import org.simplejavamail.api.mailer.config.LoadBalancingStrategy;
import org.simplejavamail.api.mailer.config.TransportStrategy;
import org.simplejavamail.internal.clisupport.therapijavadoc.TherapiJavadocHelper;
import org.simplejavamail.internal.clisupport.therapijavadoc.TherapiJavadocHelper.DocumentedMethodParam;
import org.simplejavamail.internal.clisupport.valueinterpreters.EmlFilePathToMimeMessageFunction;
import org.simplejavamail.internal.clisupport.valueinterpreters.MsgFilePathToMimeMessageFunction;
import org.simplejavamail.internal.clisupport.valueinterpreters.PemFilePathToX509CertificateFunction;
import org.simplejavamail.internal.clisupport.valueinterpreters.StringToCalendarMethodFunction;
import org.simplejavamail.internal.clisupport.valueinterpreters.StringToContentTransferEncodingFunction;
import org.simplejavamail.internal.clisupport.valueinterpreters.StringToFileFunction;
import org.simplejavamail.internal.clisupport.valueinterpreters.StringToLoadBalancingStrategyFunction;
import org.simplejavamail.internal.clisupport.valueinterpreters.StringToTransportStrategyFunction;
import org.simplejavamail.internal.util.StringUtil;
import org.simplejavamail.internal.util.StringUtil.StringFormatter;
import org.slf4j.Logger;
import java.io.File;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.EnumSet.allOf;
import static java.util.EnumSet.of;
import static java.util.regex.Pattern.compile;
import static org.bbottema.javareflection.TypeUtils.containsAnnotation;
import static org.simplejavamail.internal.util.Preconditions.assumeTrue;
import static org.simplejavamail.internal.util.Preconditions.checkNonEmptyArgument;
import static org.slf4j.LoggerFactory.getLogger;
public final class BuilderApiToPicocliCommandsMapper {
private static final Logger LOGGER = getLogger(BuilderApiToPicocliCommandsMapper.class);
/**
* These help generate the picocli labels for the support types. Results in something like:
* --someOption(=NUM)
*/
@SuppressWarnings("serial")
private static final Map, String> TYPE_LABELS = new HashMap, String>() {{
put(boolean.class, "BOOL");
put(Boolean.class, "BOOL");
put(String.class, "TEXT");
put(Object.class, "TEXT");
put(TransportStrategy.class, "NAME");
put(CalendarMethod.class, "RFC-2446 VEVENT METHOD");
put(int.class, "NUM");
put(Integer.class, "NUM");
put(MimeMessage.class, "EML FILE");
put(DataSource.class, "FILE");
put(byte[].class, "FILE");
put(InputStream.class, "FILE");
put(File.class, "FILE");
put(X509Certificate.class, "PEM FILE");
put(UUID.class, "UUID");
put(LoadBalancingStrategy.class, "NAME");
put(ContentTransferEncoding.class, "NAME");
put(Date.class, "yyyy-[M]M-[d]d[ HH:mm]");
}};
static {
ValueConversionHelper.registerValueConverter(new StringToFileFunction());
ValueConversionHelper.registerValueConverter(new EmlFilePathToMimeMessageFunction());
ValueConversionHelper.registerValueConverter(new MsgFilePathToMimeMessageFunction());
ValueConversionHelper.registerValueConverter(new PemFilePathToX509CertificateFunction());
ValueConversionHelper.registerValueConverter(new StringToTransportStrategyFunction());
ValueConversionHelper.registerValueConverter(new StringToLoadBalancingStrategyFunction());
ValueConversionHelper.registerValueConverter(new StringToCalendarMethodFunction());
ValueConversionHelper.registerValueConverter(new StringToContentTransferEncodingFunction());
}
private BuilderApiToPicocliCommandsMapper() {
}
@NotNull
static List generateOptionsFromBuilderApi(@SuppressWarnings("SameParameterValue") Class>[] relevantBuilderRootApi) {
final Set cliOptions = new TreeSet<>();
final Set> processedApiNodes = new HashSet<>();
for (Class> apiRoot : relevantBuilderRootApi) {
generateOptionsFromBuilderApiChain(apiRoot, processedApiNodes, cliOptions);
}
return new ArrayList<>(cliOptions);
}
private static void generateOptionsFromBuilderApiChain(Class> apiNode, Set> processedApiNodes, Set cliOptionsFoundSoFar) {
Class> apiNodeChainClass = apiNode;
while (apiNodeChainClass != null && apiNodeChainClass.getPackage().getName().contains("org.simplejavamail")) {
for (Class> apiInterface : apiNodeChainClass.getInterfaces()) {
generateOptionsFromBuilderApi(apiInterface, processedApiNodes, cliOptionsFoundSoFar);
}
generateOptionsFromBuilderApi(apiNodeChainClass, processedApiNodes, cliOptionsFoundSoFar);
apiNodeChainClass = apiNodeChainClass.getSuperclass();
}
}
/**
* Produces all the --option Picocli-based params for specific API class.
* Recursive for returned API class (since builders can return different builders.
*/
private static void generateOptionsFromBuilderApi(Class> apiNode, Set> processedApiNodes, Set cliOptionsFoundSoFar) {
if (processedApiNodes.contains(apiNode)) {
return;
}
processedApiNodes.add(apiNode);
for (Method m : ClassUtils.collectMethods(apiNode, apiNode, of(MethodModifier.PUBLIC))) {
val cliMethodCompatibilityResult = methodIsCliCompatible(m);
if (cliMethodCompatibilityResult.isCompatible()) {
final String optionName = determineCliOptionName(apiNode, m);
LOGGER.debug("option {} found for {}.{}({})", optionName, apiNode.getSimpleName(), m.getName(), m.getParameterTypes());
// assertion check
for (CliDeclaredOptionSpec knownOption : cliOptionsFoundSoFar) {
if (knownOption.getName().equals(optionName)) {
final boolean methodIsActuallyTheSame = knownOption.getSourceMethod().equals(m);
if (!methodIsActuallyTheSame) {
String msg = "@CliOptionNameOverride needed one of the following two methods:%n\t%s%n\t%s%n\t----------";
throw new AssertionError(format(msg, knownOption.getSourceMethod(), m));
}
}
}
cliOptionsFoundSoFar.add(new CliDeclaredOptionSpec(
optionName,
TherapiJavadocHelper.determineCliOptionDescriptions(m),
getArgumentsForCliOption(m),
apiNode.getAnnotation(Cli.BuilderApiNode.class).builderApiType(),
m));
Class> potentialNestedApiNode = m.getReturnType();
if (potentialNestedApiNode.isAnnotationPresent(Cli.BuilderApiNode.class)) {
generateOptionsFromBuilderApiChain(potentialNestedApiNode, processedApiNodes, cliOptionsFoundSoFar);
}
} else {
LOGGER.debug("Method not CLI compatible ({}): {}.{}({})",
cliMethodCompatibilityResult.getReason(), apiNode.getSimpleName(), m.getName(), Arrays.toString(m.getParameterTypes()));
}
}
}
public static CliMethodCompatibilityResult methodIsCliCompatible(Method m) {
if (!m.getDeclaringClass().isAnnotationPresent(Cli.BuilderApiNode.class)) {
return new CliMethodCompatibilityResult(false, "@BuilderApiNode missing on enclosing class");
} else if (m.isAnnotationPresent(Cli.ExcludeApi.class)) {
return new CliMethodCompatibilityResult(false, "Compatibility check failed: @ExcludeApi present");
} else if (BeanUtils.isBeanMethod(m, m.getDeclaringClass(), allOf(Visibility.class), true)) {
return new CliMethodCompatibilityResult(false, "Compatibility check failed: actually a bean method");
} else if (MethodUtils.methodHasCollectionParameter(m)) {
return new CliMethodCompatibilityResult(false, "Compatibility check failed: collection parameter present");
}
@SuppressWarnings("unchecked")
Class[] stringParameters = new Class[m.getParameterTypes().length];
Arrays.fill(stringParameters, String.class);
if (!MethodUtils.isMethodCompatible(m, allOf(LookupMode.class), stringParameters)) {
return new CliMethodCompatibilityResult(false, "Compatibility check failed: parameters not compatible");
}
return new CliMethodCompatibilityResult(true);
}
@NotNull
public static List colorizeDescriptions(List descriptions) {
List colorizedDescriptions = new ArrayList<>();
for (String description : descriptions) {
colorizedDescriptions.add(colorizeOptionsInText(description, CliColorScheme.OPTION_STYLE));
}
return colorizedDescriptions;
}
@NotNull
public static String colorizeOptionsInText(String text, String ansiStyles) {
final StringFormatter TOKEN_REPLACER = StringFormatter.formatterForPattern("@|" + ansiStyles + " %s|@");
final String optionRegex = "(?:--(?:help|version)|-(?:h|v)|(?:--?\\w+:\\w+))(?!\\w)"; // https://regex101.com/r/SOs17K/4
return StringUtil.replaceNestedTokens(text, 0, "@|", "|@", optionRegex, TOKEN_REPLACER);
}
@NotNull
public static String determineCliOptionName(Class> apiNode, Method m) {
String methodName = m.isAnnotationPresent(Cli.OptionNameOverride.class)
? m.getAnnotation(Cli.OptionNameOverride.class).value()
: m.getName();
final String cliCommandPrefix = apiNode.getAnnotation(Cli.BuilderApiNode.class).builderApiType().getParamPrefix();
assumeTrue(!cliCommandPrefix.isEmpty(), "Option prefix missing from API class");
return format("--%s:%s", cliCommandPrefix, methodName);
}
@NotNull
public static List getArgumentsForCliOption(Method m) {
final Annotation[][] annotations = m.getParameterAnnotations();
final Class>[] declaredParameters = m.getParameterTypes();
final List documentedParameters = TherapiJavadocHelper.getParamDescriptions(m);
final List cliParams = new ArrayList<>();
for (int i = 0; i < declaredParameters.length; i++) {
final Class> p = declaredParameters[i];
final DocumentedMethodParam dP = documentedParameters.get(i);
final boolean required = !containsAnnotation(asList(annotations[i]), Nullable.class);
final String javadocDescription = extractJavadocDescription(dP.getJavadoc());
final String[] javadocExamples = extractJavadocExamples(dP.getJavadoc());
cliParams.add(new CliDeclaredOptionValue(dP.getName(), determineTypeLabel(p), javadocDescription, required, javadocExamples));
}
return cliParams;
}
@NotNull
static String extractJavadocDescription(String javadoc) {
return javadoc.substring(0, determineJavadocLengthUntilExamples(javadoc, false));
}
@NotNull
static String[] extractJavadocExamples(String javadoc) {
final int javadocLengthIncludingExamples = determineJavadocLengthUntilExamples(javadoc, true);
if (javadocLengthIncludingExamples != javadoc.length()) {
return javadoc.substring(javadocLengthIncludingExamples)
.replaceAll("(?m)^\\s*-\\s*", "") // trim leading whitespace
.replaceAll("(?m)\\s*$", "") // trim trailing whitespace
.split("\\r?\\n"); // split on trimmed newlines
}
return new String[0];
}
private static int determineJavadocLengthUntilExamples(String javadoc, boolean includeExamplesTextLength) {
final Pattern PATTERN_EXAMPLES_MARKER = compile("(?i)(?s).*(? examples?:\\s*)"); // https://regex101.com/r/UMMmlV/3
final Matcher matcher = PATTERN_EXAMPLES_MARKER.matcher(javadoc);
return (matcher.find())
? matcher.end() - (!includeExamplesTextLength ? matcher.group("examples").length() : 0)
: javadoc.length();
}
@NotNull
private static String determineTypeLabel(Class> type) {
return checkNonEmptyArgument(TYPE_LABELS.get(type), "Missing type label for type " + type);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy