Skip to content

Commit

Permalink
Remove reflection from YAML parser detection (#129)
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Edgar <michael@xlate.io>
  • Loading branch information
MikeEdgar authored Feb 14, 2024
1 parent 5ae3aac commit 5931d48
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 121 deletions.
54 changes: 14 additions & 40 deletions src/main/java/io/xlate/yamljson/SettingsBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,69 +2,43 @@

import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;

import org.snakeyaml.engine.v2.api.DumpSettings;
import org.snakeyaml.engine.v2.api.DumpSettingsBuilder;
import org.snakeyaml.engine.v2.api.LoadSettings;
import org.snakeyaml.engine.v2.api.LoadSettingsBuilder;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.LoaderOptions;

interface SettingsBuilder {

static final String MOD_SNAKEYAML = "org.yaml.snakeyaml";
static final String MARKER_SNAKEYAML = "org.yaml.snakeyaml.Yaml";

static final String MOD_SNAKEYAML_ENGINE = "org.snakeyaml.engine";
static final String MARKER_SNAKEYAML_ENGINE = "org.snakeyaml.engine.v2.api.lowlevel.Parse";

static final String MISSING_MODULE_MESSAGE = "Required module not found: %s. "
+ "Ensure module is present on module path. Add to application module-info or "
+ "include with --add-modules command line option.";

default <T> T loadProvider(Supplier<T> providerSupplier, String providerModule) {
static <T> Optional<T> loadProvider(Map<String, Object> properties, Function<Map<String, Object>, T> providerFactory) {
try {
return providerSupplier.get();
return Optional.of(providerFactory.apply(properties));
} catch (Exception | NoClassDefFoundError e) {
throw new IllegalStateException(String.format(MISSING_MODULE_MESSAGE, providerModule), e);
return Optional.empty();
}
}

default LoaderOptions buildLoaderOptions(Map<String, Object> properties) {
LoaderOptions options = new LoaderOptions();
replace(properties, Yaml.Settings.LOAD_MAX_ALIAS_EXPANSION_SIZE, Long::valueOf, Long.MAX_VALUE);
// No load properties supported currently
return options;
}

default DumperOptions buildDumperOptions(Map<String, Object> properties) {
DumperOptions settings = new DumperOptions();
settings.setExplicitStart(getProperty(properties, Yaml.Settings.DUMP_EXPLICIT_START, Boolean::valueOf, false));
settings.setExplicitEnd(getProperty(properties, Yaml.Settings.DUMP_EXPLICIT_END, Boolean::valueOf, false));
return settings;
}

default LoadSettings buildLoadSettings(Map<String, Object> properties) {
LoadSettingsBuilder settings = LoadSettings.builder();
settings.setUseMarks(getProperty(properties, Yaml.Settings.LOAD_USE_MARKS, Boolean::valueOf, true));
replace(properties, Yaml.Settings.LOAD_MAX_ALIAS_EXPANSION_SIZE, Long::valueOf, Long.MAX_VALUE);
return settings.build();
static <T> T loadProvider(Map<String, Object> properties, Function<Map<String, Object>, T> providerFactory, String providerModule) {
try {
return providerFactory.apply(properties);
} catch (Exception | NoClassDefFoundError e) {
throw new IllegalStateException(String.format(MISSING_MODULE_MESSAGE, providerModule), e);
}
}

default DumpSettings buildDumpSettings(Map<String, Object> properties) {
DumpSettingsBuilder settings = DumpSettings.builder();
settings.setExplicitStart(getProperty(properties, Yaml.Settings.DUMP_EXPLICIT_START, Boolean::valueOf, false));
settings.setExplicitEnd(getProperty(properties, Yaml.Settings.DUMP_EXPLICIT_END, Boolean::valueOf, false));
return settings.build();
static IllegalStateException noProvidersFound() {
return new IllegalStateException("No YAML providers found on class/module path!");
}

default <T> T getProperty(Map<String, ?> properties, String key, Function<String, T> parser, T defaultValue) {
static <T> T getProperty(Map<String, ?> properties, String key, Function<String, T> parser, T defaultValue) {
return parser.apply(String.valueOf(Objects.requireNonNullElse(properties.get(key), defaultValue)));
}

default <T> void replace(Map<String, Object> properties, String key, Function<String, T> parser, T defaultValue) {
static <T> void replace(Map<String, Object> properties, String key, Function<String, T> parser, T defaultValue) {
properties.put(key, getProperty(properties, key, parser, defaultValue));
}
}
34 changes: 2 additions & 32 deletions src/main/java/io/xlate/yamljson/Yaml.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import jakarta.json.JsonArray;
import jakarta.json.JsonException;
Expand All @@ -33,7 +30,6 @@
import jakarta.json.JsonReaderFactory;
import jakarta.json.JsonWriter;
import jakarta.json.JsonWriterFactory;
import jakarta.json.spi.JsonProvider;
import jakarta.json.stream.JsonGenerator;
import jakarta.json.stream.JsonGeneratorFactory;
import jakarta.json.stream.JsonParser;
Expand Down Expand Up @@ -77,35 +73,9 @@ public final class Yaml {
public static final class Versions {
public static final String V1_1 = "v1.1";
public static final String V1_2 = "v1.2";
static final Set<String> VERSIONS_PRESENT;

private interface ClassSupplier {
Class<?> get() throws LinkageError, ClassNotFoundException;
}

static {
Set<String> versions = new LinkedHashSet<>(2);
addIfPresent(versions, V1_1, () -> Class.forName(SettingsBuilder.MARKER_SNAKEYAML));
addIfPresent(versions, V1_2, () -> Class.forName(SettingsBuilder.MARKER_SNAKEYAML_ENGINE));
VERSIONS_PRESENT = Collections.unmodifiableSet(versions);
}

private Versions() {
}

private static void addIfPresent(Set<String> versions, String version, ClassSupplier loader) {
try {
loader.get();
versions.add(version);
} catch (ClassNotFoundException | LinkageError e) {
// Ignored
}
}

static Set<String> supportedVersions() {
return VERSIONS_PRESENT;
}

}

/**
Expand Down Expand Up @@ -148,12 +118,12 @@ private Settings() {
public static final String DUMP_EXPLICIT_END = "DUMP_EXPLICIT_END";
}

private static final JsonProvider PROVIDER = new YamlProvider();
private static final YamlProvider PROVIDER = new YamlProvider();

private Yaml() {
}

private static JsonProvider provider() {
private static YamlProvider provider() {
return PROVIDER;
}

Expand Down
54 changes: 50 additions & 4 deletions src/main/java/io/xlate/yamljson/YamlGeneratorFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
*/
package io.xlate.yamljson;

import static io.xlate.yamljson.SettingsBuilder.getProperty;
import static io.xlate.yamljson.SettingsBuilder.loadProvider;

import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
Expand All @@ -24,12 +27,45 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;

import jakarta.json.stream.JsonGenerator;
import jakarta.json.stream.JsonGeneratorFactory;

import org.snakeyaml.engine.v2.api.DumpSettings;
import org.snakeyaml.engine.v2.api.DumpSettingsBuilder;
import org.yaml.snakeyaml.DumperOptions;

class YamlGeneratorFactory implements JsonGeneratorFactory, SettingsBuilder {

private static final String SNAKEYAML_ENGINE_PROVIDER = "org.snakeyaml.engine.v2.api.DumpSettings";

static final Function<Map<String, Object>, Object> SNAKEYAML_FACTORY =
props -> buildDumperOptions(props);

static final Function<Map<String, Object>, Object> SNAKEYAML_ENGINE_FACTORY =
props -> buildDumpSettings(props);

static void copyBoolean(Map<String, Object> properties, String name, Consumer<Boolean> target) {
target.accept(getProperty(properties, name, Boolean::valueOf, false));
}

static DumperOptions buildDumperOptions(Map<String, Object> properties) {
DumperOptions options = new DumperOptions();
copyBoolean(properties, Yaml.Settings.DUMP_EXPLICIT_START, options::setExplicitStart);
copyBoolean(properties, Yaml.Settings.DUMP_EXPLICIT_END, options::setExplicitEnd);
return options;
}

static DumpSettings buildDumpSettings(Map<String, Object> properties) {
DumpSettingsBuilder settings = DumpSettings.builder();
copyBoolean(properties, Yaml.Settings.DUMP_EXPLICIT_START, settings::setExplicitStart);
copyBoolean(properties, Yaml.Settings.DUMP_EXPLICIT_END, settings::setExplicitEnd);
return settings.build();
}

private final Map<String, Object> properties;
private final boolean useSnakeYamlEngine;
private final Object snakeYamlSettings;
Expand All @@ -38,12 +74,22 @@ class YamlGeneratorFactory implements JsonGeneratorFactory, SettingsBuilder {
this.properties = new HashMap<>(properties);

Object version = properties.get(Yaml.Settings.YAML_VERSION);
useSnakeYamlEngine = Yaml.Versions.V1_2.equals(version);

if (useSnakeYamlEngine) {
snakeYamlSettings = loadProvider(() -> buildDumpSettings(this.properties), MOD_SNAKEYAML_ENGINE);
if (version == null) {
snakeYamlSettings = Optional.empty()
.or(() -> loadProvider(this.properties, SNAKEYAML_FACTORY))
.or(() -> loadProvider(this.properties, SNAKEYAML_ENGINE_FACTORY))
.orElseThrow(SettingsBuilder::noProvidersFound);

useSnakeYamlEngine = SNAKEYAML_ENGINE_PROVIDER.equals(snakeYamlSettings.getClass().getName());
} else {
snakeYamlSettings = loadProvider(() -> buildDumperOptions(this.properties), MOD_SNAKEYAML);
useSnakeYamlEngine = Yaml.Versions.V1_2.equals(version);

if (useSnakeYamlEngine) {
snakeYamlSettings = loadProvider(this.properties, SNAKEYAML_ENGINE_FACTORY, MOD_SNAKEYAML_ENGINE);
} else {
snakeYamlSettings = loadProvider(this.properties, SNAKEYAML_FACTORY, MOD_SNAKEYAML);
}
}
}

Expand Down
64 changes: 47 additions & 17 deletions src/main/java/io/xlate/yamljson/YamlParserFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
*/
package io.xlate.yamljson;

import static io.xlate.yamljson.SettingsBuilder.getProperty;
import static io.xlate.yamljson.SettingsBuilder.loadProvider;
import static io.xlate.yamljson.SettingsBuilder.replace;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
Expand All @@ -23,45 +27,71 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;

import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.stream.JsonParser;
import jakarta.json.stream.JsonParserFactory;

import org.snakeyaml.engine.v2.api.LoadSettings;
import org.yaml.snakeyaml.LoaderOptions;

class YamlParserFactory implements JsonParserFactory, SettingsBuilder {

private static final String SNAKEYAML_ENGINE_PROVIDER = "org.snakeyaml.engine.v2.api.lowlevel.Parse";

static final Function<Map<String, Object>, Object> SNAKEYAML_FACTORY =
// No load properties supported currently
props -> new org.yaml.snakeyaml.Yaml(new LoaderOptions());

static final Function<Map<String, Object>, Object> SNAKEYAML_ENGINE_FACTORY =
props -> new org.snakeyaml.engine.v2.api.lowlevel.Parse(buildLoadSettings(props));

static LoadSettings buildLoadSettings(Map<String, Object> properties) {
return LoadSettings.builder()
.setUseMarks(getProperty(properties, Yaml.Settings.LOAD_USE_MARKS, Boolean::valueOf, true))
.build();
}

private final Map<String, Object> properties;
private final boolean useSnakeYamlEngine;
private final Object snakeYamlProvider;
private final Function<InputStream, Reader> yamlReaderProvider;

YamlParserFactory(Map<String, ?> properties) {
this.properties = new HashMap<>(properties);

Object version = properties.get(Yaml.Settings.YAML_VERSION);
useSnakeYamlEngine = Yaml.Versions.V1_2.equals(version);

if (useSnakeYamlEngine) {
snakeYamlProvider = loadProvider(() -> new org.snakeyaml.engine.v2.api.lowlevel.Parse(buildLoadSettings(this.properties)),
MOD_SNAKEYAML_ENGINE);
if (version == null) {
snakeYamlProvider = Optional.empty()
.or(() -> loadProvider(this.properties, SNAKEYAML_FACTORY))
.or(() -> loadProvider(this.properties, SNAKEYAML_ENGINE_FACTORY))
.orElseThrow(SettingsBuilder::noProvidersFound);

useSnakeYamlEngine = SNAKEYAML_ENGINE_PROVIDER.equals(snakeYamlProvider.getClass().getName());
} else {
snakeYamlProvider = loadProvider(() -> new org.yaml.snakeyaml.Yaml(buildLoaderOptions(this.properties)),
MOD_SNAKEYAML);
useSnakeYamlEngine = Yaml.Versions.V1_2.equals(version);

if (useSnakeYamlEngine) {
snakeYamlProvider = loadProvider(this.properties, SNAKEYAML_ENGINE_FACTORY, MOD_SNAKEYAML_ENGINE);
} else {
snakeYamlProvider = loadProvider(this.properties, SNAKEYAML_FACTORY, MOD_SNAKEYAML);
}
}
}

YamlParser<?, ?> createYamlParser(InputStream stream) { // NOSONAR - ignore use of wildcards
Reader reader;
yamlReaderProvider = useSnakeYamlEngine
? org.snakeyaml.engine.v2.api.YamlUnicodeReader::new
: org.yaml.snakeyaml.reader.UnicodeReader::new;

if (useSnakeYamlEngine) {
reader = loadProvider(() -> new org.snakeyaml.engine.v2.api.YamlUnicodeReader(stream),
MOD_SNAKEYAML_ENGINE);
} else {
reader = loadProvider(() -> new org.yaml.snakeyaml.reader.UnicodeReader(stream),
MOD_SNAKEYAML);
}
// Ensure this property is always set, defaulting to Long.MAX_VALUE
replace(this.properties, Yaml.Settings.LOAD_MAX_ALIAS_EXPANSION_SIZE, Long::valueOf, Long.MAX_VALUE);
}

return createYamlParser(reader);
YamlParser<?, ?> createYamlParser(InputStream stream) { // NOSONAR - ignore use of wildcards
return createYamlParser(yamlReaderProvider.apply(stream));
}

YamlParser<?, ?> createYamlParser(Reader reader) { // NOSONAR - ignore use of wildcards
Expand Down
17 changes: 3 additions & 14 deletions src/main/java/io/xlate/yamljson/YamlProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

import jakarta.json.JsonArrayBuilder;
import jakarta.json.JsonBuilderFactory;
Expand All @@ -48,20 +48,9 @@ final class YamlProvider extends JsonProvider {
private final YamlReaderFactory defaultReaderFactory;
private final YamlGeneratorFactory defaultGeneratorFactory;
private final YamlWriterFactory defaultWriterFactory;
final String defaultVersion;

public YamlProvider() {
Set<String> supportedVersions = Yaml.Versions.supportedVersions();

if (supportedVersions.isEmpty()) {
throw new IllegalStateException("No YAML providers found on class/module path!");
} else {
// v1.1 if available, otherwise v1.2
defaultVersion = supportedVersions.iterator().next();
}

var defaultProperties = Map.of(Yaml.Settings.YAML_VERSION, defaultVersion);

YamlProvider() {
Map<String, ?> defaultProperties = Collections.emptyMap();
defaultParserFactory = new YamlParserFactory(defaultProperties);
defaultReaderFactory = new YamlReaderFactory(defaultParserFactory);
defaultGeneratorFactory = new YamlGeneratorFactory(defaultProperties);
Expand Down
Loading

0 comments on commit 5931d48

Please sign in to comment.