001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.databind.io.yaml;
007
008import org.json.JSONException;
009import org.json.JSONObject;
010import org.yaml.snakeyaml.DumperOptions;
011import org.yaml.snakeyaml.LoaderOptions;
012import org.yaml.snakeyaml.Yaml;
013import org.yaml.snakeyaml.constructor.Constructor;
014import org.yaml.snakeyaml.nodes.Tag;
015import org.yaml.snakeyaml.representer.Representer;
016import org.yaml.snakeyaml.resolver.Resolver;
017
018import java.io.BufferedInputStream;
019import java.io.IOException;
020import java.net.URI;
021import java.util.Map;
022
023import dev.metaschema.core.util.ObjectUtils;
024import edu.umd.cs.findbugs.annotations.NonNull;
025
026/**
027 * Utility methods for YAML parsing and conversion operations.
028 */
029public final class YamlOperations {
030  /**
031   * Thread-local Yaml parser to ensure thread safety. SnakeYAML's Yaml class is
032   * not thread-safe, so each thread needs its own instance.
033   */
034  private static final ThreadLocal<Yaml> YAML_PARSER = ThreadLocal.withInitial(() -> {
035    LoaderOptions loaderOptions = new LoaderOptions();
036    loaderOptions.setCodePointLimit(Integer.MAX_VALUE - 1); // 2GB
037    Constructor constructor = new Constructor(loaderOptions);
038    DumperOptions dumperOptions = new DumperOptions();
039    Representer representer = new Representer(dumperOptions);
040    return new Yaml(constructor, representer, dumperOptions, loaderOptions, new Resolver() {
041      @Override
042      protected void addImplicitResolvers() {
043        addImplicitResolver(Tag.BOOL, BOOL, "yYnNtTfFoO");
044        addImplicitResolver(Tag.INT, INT, "-+0123456789");
045        addImplicitResolver(Tag.FLOAT, FLOAT, "-+0123456789.");
046        addImplicitResolver(Tag.MERGE, MERGE, "<");
047        addImplicitResolver(Tag.NULL, NULL, "~nN\0");
048        addImplicitResolver(Tag.NULL, EMPTY, null);
049        // addImplicitResolver(Tag.TIMESTAMP, TIMESTAMP, "0123456789");
050      }
051    });
052  });
053
054  private YamlOperations() {
055    // disable construction
056  }
057
058  /**
059   * Parse the data represented in YAML in the provided {@code target}, producing
060   * an mapping of field names to Java object values.
061   *
062   * @param target
063   *          the YAML file to parse
064   * @return the mapping of field names to Java object values
065   * @throws IOException
066   *           if an error occurred while parsing the YAML content
067   */
068  @SuppressWarnings({ "unchecked", "null" })
069  @NonNull
070  public static Map<String, Object> parseYaml(URI target) throws IOException {
071    try (BufferedInputStream is = new BufferedInputStream(ObjectUtils.notNull(target.toURL().openStream()))) {
072      return (Map<String, Object>) YAML_PARSER.get().load(is);
073    }
074  }
075
076  /**
077   * Converts the provided YAML {@code map} into JSON.
078   *
079   * @param map
080   *          the YAML map
081   * @return the JSON object
082   * @throws JSONException
083   *           if an error occurred while building the JSON tree
084   */
085  public static JSONObject yamlToJson(@NonNull Map<String, Object> map) {
086    return new JSONObject(map);
087  }
088}