JavaMailClasspathConsistencyCondition.java

package fr.sii.ogham.spring.email.condition;

import org.springframework.boot.autoconfigure.condition.*;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

import java.util.ArrayList;
import java.util.List;

import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.springframework.boot.autoconfigure.condition.ConditionMessage.of;

public class JavaMailClasspathConsistencyCondition extends SpringBootCondition {

    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // TODO: only if configuration properties are set ?
        ConditionOutcome javaxOutcome = checkJavaxMailConsistency(context, metadata);
        ConditionOutcome jakartaOutcome = checkJakartaMailConsistency(context, metadata);
        return new ConditionOutcome(
                !isClasspathConsistent(javaxOutcome, jakartaOutcome),
                of(asList(javaxOutcome.getConditionMessage(), jakartaOutcome.getConditionMessage())));
    }

    private static boolean isClasspathConsistent(ConditionOutcome javaxOutcome, ConditionOutcome jakartaOutcome) {
        return javaxOutcome.isMatch() || jakartaOutcome.isMatch();
    }


    private static ConditionOutcome checkJavaxMailConsistency(ConditionContext context, AnnotatedTypeMetadata metadata) {
        ConditionInfoMerger merger = new ConditionInfoMerger();
        merger.addCondition(isJavaxMailApiPresent(context),
                "javax.mail package is present in the classpath",
                "javax.mail is not present in the classpath => ignoring");
        if (isJavaxMailApiPresent(context)) {
            // possible version downgrade with Sprint Boot <= 2
            // => possible clash with Ogham
            // => check classpath
            merger.addCondition(isJavaxActivationApiPresent(context),
                    "javax.activation.DataHandler is present in the classpath",
                    "javax.activation.DataHandler is not present in the classpath (dependency jakarta.activation:jakarta.activation-api <= 1.2.2 may not be present in the classpath)");
            merger.addCondition(isSunMailImplementationPresent(context),
                    "com.sun.mail package is present in the classpath",
                    "com.sun.mail package is not present in the classpath (dependency com.sun.mail:javax.mail <= 1.6.2 or com.sun.mail:jakarta.mail <= 1.6.7 may not be present in the classpath)");
            merger.addOutcome(new JavaxMailServiceProvidersAvailable().getMatchOutcome(context, metadata));
            merger.addOutcome(new JavaxActivationDataHandlersAvailable().getMatchOutcome(context, metadata));
        }
        return merger.get();
    }


    private static boolean isJavaxMailApiPresent(ConditionContext context) {
        return exists(context, "javax.mail.internet.MimeMessage");
    }

    private static boolean isJavaxActivationApiPresent(ConditionContext context) {
        return exists(context, "javax.activation.MimeType")
                && exists(context, "javax.activation.DataHandler");
    }

    private static boolean isSunMailImplementationPresent(ConditionContext context) {
        return exists(context, "com.sun.mail.util.MailLogger");
    }

    private static ConditionOutcome checkJakartaMailConsistency(ConditionContext context, AnnotatedTypeMetadata metadata) {
        ConditionInfoMerger merger = new ConditionInfoMerger();
        merger.addCondition(isAngusMailImplementationPresent(context),
                "org.eclipse.angus:angus-mail is present in the classpath",
                "org.eclipse.angus:angus-mail is not present in the classpath => ignoring");
        if (isAngusMailImplementationPresent(context)) {
            // possible version downgrade with Sprint Boot <= 2
            // => possible clash with Ogham
            // => check classpath
            merger.addCondition(isJakartaMailApiPresent(context),
                    "jakarta.mail package is present in the classpath",
                    "jakarta.mail package is not present in the classpath (dependency jakarta.mail:jakarta.mail-api >= 2.0.0 is not present in the classpath)");
            merger.addCondition(isJakartaActivationApiPresent(context),
                    "jakarta.activation.DataHandler is present in the classpath",
                    "jakarta.activation.DataHandler is not present in the classpath (dependency jakarta.activation:jakarta.activation-api >= 2.0.0 is not present in the classpath)");
            merger.addOutcome(new JakartaMailServiceProvidersAvailable().getMatchOutcome(context, metadata));
            merger.addOutcome(new JakartaActivationDataHandlersAvailable().getMatchOutcome(context, metadata));
        }
        return merger.get();
    }

    private static boolean isAngusMailImplementationPresent(ConditionContext context) {
        return exists(context, "org.eclipse.angus.mail.imap.IMAPProvider");
    }

    private static boolean isJakartaMailApiPresent(ConditionContext context) {
        return exists(context, "jakarta.mail.internet.MimeMessage");
    }

    private static boolean isJakartaActivationApiPresent(ConditionContext context) {
        return exists(context, "jakarta.activation.MimeType")
                && exists(context, "jakarta.activation.DataHandler");
    }

    private static boolean exists(ConditionContext context, String className) {
        try {
            Class.forName(className, false, context.getClassLoader());
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        } catch (NoClassDefFoundError e) {
            return !isForSameClass(className, e);
        }
    }

    private static boolean isForSameClass(String className, NoClassDefFoundError e) {
        return className.equals(e.getMessage().replace("/", "."));
    }


    private static class ConditionInfoMerger {
        private final List<ConditionOutcome> outcomes = new ArrayList<>();

        public void addCondition(boolean condition, String messageIfTrue, String messageIfFalse) {
            addOutcome(new ConditionOutcome(condition, condition ? messageIfTrue : messageIfFalse));
        }

        public void addOutcome(ConditionOutcome outcome) {
            outcomes.add(outcome);
        }

        public ConditionOutcome get() {
            boolean matchAll = outcomes.stream().allMatch(ConditionOutcome::isMatch);
            List<ConditionMessage> messages = outcomes.stream().map(ConditionOutcome::getConditionMessage).collect(toList());
            return new ConditionOutcome(matchAll, ConditionMessage.of(messages));
        }
    }
}