diff --git a/modules/cli-module/pom.xml b/modules/cli-module/pom.xml index 6ff24a6d..0ed5ab4a 100644 --- a/modules/cli-module/pom.xml +++ b/modules/cli-module/pom.xml @@ -87,6 +87,20 @@ true + + + com.esotericsoftware + kryo + 5.0.0-RC1 + compile + + + de.javakaffee + kryo-serializers + 0.45 + compile + + org.apache.logging.log4j diff --git a/modules/cli-module/src/main/java/org/simplejavamail/internal/clisupport/CliDataLocator.java b/modules/cli-module/src/main/java/org/simplejavamail/internal/clisupport/CliDataLocator.java new file mode 100644 index 00000000..4ca53288 --- /dev/null +++ b/modules/cli-module/src/main/java/org/simplejavamail/internal/clisupport/CliDataLocator.java @@ -0,0 +1,30 @@ +package org.simplejavamail.internal.clisupport; + +import java.io.File; +import java.net.URISyntaxException; +import java.net.URL; + +/** + * This helper makes sure we can load cli.data no matter how the code is run (from mvn, intellij, command line). + */ +public class CliDataLocator { + public static String determinelocateCLIDataFile() { + // the following is needed bacause this is a project with submodules + // and it changes depending on how the code is executed + if (new File("src/test/resources/log4j2.xml").exists()) { + return "src/main/resources/cli.data"; + } else if (new File("modules/cli-module/src/test/resources/log4j2.xml").exists()) { + return "modules/cli-module/src/main/resources/cli.data"; + } else { + URL resource = CliDataLocator.class.getClassLoader().getResource("log4j2.xml"); + if (resource != null) { + try { + return new File(resource.toURI()).getParentFile().getPath() + "/cli.data"; + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + throw new AssertionError("Was unable to locate resources folder. Did you delete log4j2.xml?"); + } + } +} diff --git a/modules/cli-module/src/main/java/org/simplejavamail/internal/clisupport/CliSupport.java b/modules/cli-module/src/main/java/org/simplejavamail/internal/clisupport/CliSupport.java index 9d6b9d2d..a8f50a02 100644 --- a/modules/cli-module/src/main/java/org/simplejavamail/internal/clisupport/CliSupport.java +++ b/modules/cli-module/src/main/java/org/simplejavamail/internal/clisupport/CliSupport.java @@ -1,43 +1,68 @@ package org.simplejavamail.internal.clisupport; +import org.jetbrains.annotations.TestOnly; import org.simplejavamail.api.email.EmailStartingBuilder; import org.simplejavamail.api.internal.clisupport.model.CliDeclaredOptionSpec; import org.simplejavamail.api.internal.clisupport.model.CliReceivedCommand; import org.simplejavamail.api.mailer.MailerFromSessionBuilder; import org.simplejavamail.api.mailer.MailerRegularBuilder; +import org.simplejavamail.internal.clisupport.serialization.SerializationUtil; +import org.simplejavamail.internal.util.MiscUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import picocli.CommandLine; import picocli.CommandLine.ParseResult; +import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import static org.simplejavamail.internal.clisupport.BuilderApiToPicocliCommandsMapper.generateOptionsFromBuilderApi; import static org.simplejavamail.internal.clisupport.CliCommandLineProducer.configurePicoCli; +import static org.simplejavamail.internal.clisupport.CliExecutionException.ERROR_INVOKING_BUILDER_API; public class CliSupport { - + private static final Logger LOGGER = LoggerFactory.getLogger(CliSupport.class); + private static final int CONSOLE_TEXT_WIDTH = 150; - - private static final Class[] RELEVANT_BUILDER_ROOT_API = {EmailStartingBuilder.class, MailerRegularBuilder.class, MailerFromSessionBuilder.class}; - private static final List DECLARED_OPTIONS = generateOptionsFromBuilderApi(RELEVANT_BUILDER_ROOT_API); - private static final CommandLine PICOCLI_COMMAND_LINE = configurePicoCli(DECLARED_OPTIONS, CONSOLE_TEXT_WIDTH); + + private static final File CLI_DATAFILE = new File(CliDataLocator.determinelocateCLIDataFile()); + + private static final Class[] RELEVANT_BUILDER_ROOT_API = new Class[] { EmailStartingBuilder.class, MailerRegularBuilder.class, MailerFromSessionBuilder.class }; + private static final List DECLARED_OPTIONS = produceCliDeclaredOptionSpec(); + private static final CommandLine PICOCLI_COMMAND_LINE = configurePicoCli(DECLARED_OPTIONS, CONSOLE_TEXT_WIDTH); + + private static List produceCliDeclaredOptionSpec() { + try { + if (!CLI_DATAFILE.exists()) { + LOGGER.info("Initial cli.data not found, writing to (one time action): {}", CLI_DATAFILE); + List serializable = generateOptionsFromBuilderApi(RELEVANT_BUILDER_ROOT_API); + MiscUtil.writeFileBytes(CLI_DATAFILE, SerializationUtil.serialize(serializable)); + } + return SerializationUtil.deserialize(MiscUtil.readFileBytes(CLI_DATAFILE)); + } catch (IOException e) { + throw new CliExecutionException(ERROR_INVOKING_BUILDER_API, e); + } + } public static void runCLI(String[] args) { ParseResult pr = PICOCLI_COMMAND_LINE.parseArgs(cutOffAtHelp(args)); - + if (!CliCommandLineConsumerUsageHelper.processAndApplyHelp(pr, CONSOLE_TEXT_WIDTH)) { CliReceivedCommand cliReceivedOptionData = CliCommandLineConsumer.consumeCommandLineInput(pr, DECLARED_OPTIONS); CliCommandLineConsumerResultHandler.processCliResult(cliReceivedOptionData); } } - + + @TestOnly public static void listUsagesForAllOptions() { for (CliDeclaredOptionSpec declaredOption : DECLARED_OPTIONS) { - runCLI(new String[]{"send", declaredOption.getName() + "--help"}); + runCLI(new String[] { "send", declaredOption.getName() + "--help" }); System.out.print("\n\n\n"); } } - + /** * This is needed to avoid picocli to error out on other --params that are misconfigured. */ @@ -49,11 +74,11 @@ private static String[] cutOffAtHelp(String[] args) { break; } } - + if (argsToKeep.isEmpty()) { argsToKeep.add("--help"); } - + return argsToKeep.toArray(new String[]{}); } } \ No newline at end of file diff --git a/modules/cli-module/src/main/java/org/simplejavamail/internal/clisupport/serialization/SerializationUtil.java b/modules/cli-module/src/main/java/org/simplejavamail/internal/clisupport/serialization/SerializationUtil.java new file mode 100644 index 00000000..6e18edf5 --- /dev/null +++ b/modules/cli-module/src/main/java/org/simplejavamail/internal/clisupport/serialization/SerializationUtil.java @@ -0,0 +1,60 @@ +package org.simplejavamail.internal.clisupport.serialization; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; +import com.esotericsoftware.kryo.serializers.FieldSerializer; +import com.esotericsoftware.kryo.serializers.JavaSerializer; +import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy; +import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer; +import org.jetbrains.annotations.NotNull; +import org.objenesis.strategy.StdInstantiatorStrategy; + +import java.io.IOException; + +/** + * Used to serialize attachments of nested Outlook messages. This is needed because outlook-message-parser returns a Java structure from a .msg source, but this conversion is 1-way. An Email object + * represents attachments as DataSources however, so for this we need to serialize back from the java structure to a binary format. This Util does this. + *
+ * Then for users to obtain the Javastructure again, they must use this util to deserialize the relevant attachment. + * + * @see GitHub issue #314 + */ +public class SerializationUtil { + + private static final Kryo KRYO = initKryo(); + + @NotNull + private static Kryo initKryo() { + Kryo kryo = new Kryo(); + kryo.setRegistrationRequired(false); + kryo.setInstantiatorStrategy(new DefaultInstantiatorStrategy(new StdInstantiatorStrategy())); +// final FieldSerializer.FieldSerializerConfig config = new FieldSerializer.FieldSerializerConfig(); +// config.setSerializeTransient(true); + UnmodifiableCollectionsSerializer.registerSerializers(kryo); + return kryo; + } + + private static void registerClassSerializer(final Kryo kryo, final FieldSerializer.FieldSerializerConfig config, Class type) { + kryo.register(type, new FieldSerializer(kryo, type, config)); + } + + private static void registerClassJavaSerializer(final Kryo kryo, Class type) { + kryo.register(type, new JavaSerializer()); + } + + @NotNull + public static byte[] serialize(@NotNull final Object serializable) + throws IOException { + final Output output = new Output(1024, -1); + KRYO.writeClassAndObject(output, serializable); + return output.toBytes(); + } + + @SuppressWarnings("unchecked") + @NotNull + public static T deserialize(@NotNull final byte[] serialized) + throws IOException { + return (T) KRYO.readClassAndObject(new Input(serialized)); + } +}