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}