YAMLUtils.java
/*
* GovWay - A customizable API Gateway
* https://govway.org
*
* Copyright (c) 2005-2025 Link.it srl (https://link.it).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3, as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.openspcoop2.utils.json;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openspcoop2.utils.SemaphoreLock;
import org.openspcoop2.utils.UtilsException;
import org.slf4j.Logger;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
/**
* YAMLUtils
*
* @author Poli Andrea (apoli@link.it)
* @author $Author$
* @version $Rev$, $Date$
*/
public class YAMLUtils extends AbstractUtils {
static {
YamlSnakeLimits.initialize();
}
private static YAMLUtils yamlUtils = null;
private static YAMLUtils yamlUtilsPretty = null;
private static synchronized void init(boolean prettyPrint){
if(prettyPrint) {
if(YAMLUtils.yamlUtilsPretty==null){
YAMLUtils.yamlUtilsPretty = new YAMLUtils(true);
}
}
else {
if(YAMLUtils.yamlUtils==null){
YAMLUtils.yamlUtils = new YAMLUtils(false);
}
}
}
public static YAMLUtils getInstance(){
return getInstance(false);
}
public static YAMLUtils getInstance(boolean prettyPrint){
if(prettyPrint) {
if(YAMLUtils.yamlUtilsPretty==null){
// spotbugs warning 'SING_SINGLETON_GETTER_NOT_SYNCHRONIZED'
synchronized (YAMLUtils.class) {
YAMLUtils.init(true);
}
}
return YAMLUtils.yamlUtilsPretty;
}
else {
if(YAMLUtils.yamlUtils==null){
// spotbugs warning 'SING_SINGLETON_GETTER_NOT_SYNCHRONIZED'
synchronized (YAMLUtils.class) {
YAMLUtils.init(false);
}
}
return YAMLUtils.yamlUtils;
}
}
private static org.openspcoop2.utils.Semaphore semaphore = new org.openspcoop2.utils.Semaphore("JSONUtils");
private static YAMLMapper internalMapper;
private static synchronized void initMapper() {
SemaphoreLock lock = semaphore.acquireThrowRuntime("initMapper");
try {
if(internalMapper==null){
internalMapper = new YAMLMapper();
internalMapper.setTimeZone(TimeZone.getDefault());
internalMapper.setSerializationInclusion(Include.NON_NULL);
internalMapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
internalMapper.configure(com.fasterxml.jackson.databind.SerializationFeature.
WRITE_DATES_AS_TIMESTAMPS , false);
// Since 2.1.4, the field exampleSetFlag appears in json produced with ObjectMapper#writeValue(File, Object) when serializing an object of type OpenApi.
// "exampleSetFlag" : false
// Con il codice sottostante, nelle classi Mixin l'attributo 'getExampleSetFlag' viene ignorato
internalMapper.addMixIn(io.swagger.v3.oas.models.media.Schema.class, io.swagger.v3.core.jackson.mixin.SchemaMixin.class);
internalMapper.addMixIn(io.swagger.v3.oas.models.media.MediaType.class, io.swagger.v3.core.jackson.mixin.MediaTypeMixin.class);
}
}finally {
semaphore.release(lock, "initMapper");
}
}
public static void setMapperTimeZone(TimeZone timeZone) {
if(internalMapper==null){
initMapper();
}
SemaphoreLock lock = semaphore.acquireThrowRuntime("setMapperTimeZone");
try {
internalMapper.setTimeZone(timeZone);
}finally {
semaphore.release(lock, "setMapperTimeZone");
}
}
public static void registerJodaModule() {
if(internalMapper==null){
initMapper();
}
SemaphoreLock lock = semaphore.acquireThrowRuntime("registerJodaModule");
try {
internalMapper.registerModule(new JodaModule());
}finally {
semaphore.release(lock, "registerJodaModule");
}
}
public static void registerJavaTimeModule() {
if(internalMapper==null){
initMapper();
}
SemaphoreLock lock = semaphore.acquireThrowRuntime("registerJavaTimeModule");
try {
internalMapper.registerModule(new JavaTimeModule());
}finally {
semaphore.release(lock, "registerJavaTimeModule");
}
}
public static YAMLMapper getObjectMapper() {
if(internalMapper==null){
initMapper();
}
return internalMapper;
}
private static ObjectWriter writer;
private static synchronized void initWriter() {
if(internalMapper==null){
initMapper();
}
if(writer==null){
writer = internalMapper.writer();
}
}
public static ObjectWriter getObjectWriter() {
if(writer==null){
initWriter();
}
return writer;
}
private static ObjectWriter writerPrettyPrint;
private static synchronized void initWriterPrettyPrint() {
if(internalMapper==null){
initMapper();
}
if(writerPrettyPrint==null){
writerPrettyPrint = internalMapper.writer().withDefaultPrettyPrinter();
}
}
public static ObjectWriter getObjectWriterPrettyPrint() {
if(writerPrettyPrint==null){
initWriterPrettyPrint();
}
return writerPrettyPrint;
}
private YAMLUtils(boolean prettyPrint) {
super(prettyPrint);
}
@Override
protected void _initMapper() {
initMapper();
}
@Override
protected void _initWriter(boolean prettyPrint) {
if(prettyPrint) {
initWriterPrettyPrint();
}
else {
initWriter();
}
}
@Override
protected ObjectMapper _getObjectMapper() {
return getObjectMapper();
}
@Override
protected ObjectWriter _getObjectWriter(boolean prettyPrint) {
if(prettyPrint) {
return getObjectWriterPrettyPrint();
}
else {
return getObjectWriter();
}
}
// IS
public boolean isYaml(byte[]jsonBytes){
return !JSONUtils.getInstance().isJson(jsonBytes) && this.isValid(jsonBytes);
}
public boolean isYaml(String jsonString){
return !JSONUtils.getInstance().isJson(jsonString) && this.isValid(jsonString);
}
// UTILS per ANCHOR
public static boolean containsKeyAnchor(String yaml) {
return containsMergeKeyAnchor(yaml) || containsRefKeyAnchor(yaml);
}
public static boolean containsMergeKeyAnchor(String yaml) {
return yaml!=null && yaml.contains("<<: *");
}
public static boolean containsRefKeyAnchor(String yaml) {
return yaml!=null && yaml.contains(" *") && yaml.contains(" &");
}
public static String resolveMergeKeyAndConvertToJson(String yaml) throws UtilsException {
return resolveMergeKeyAndConvertToJson(yaml, JSONUtils.getInstance());
}
public static String resolveMergeKeyAndConvertToJson(String yaml, JSONUtils jsonUtils) throws UtilsException {
return resolveAnchorAndConvertToJson(yaml, jsonUtils, false);
}
public static String resolveKeyAnchorAndConvertToJson(String yaml) throws UtilsException {
return resolveKeyAnchorAndConvertToJson(yaml, JSONUtils.getInstance());
}
public static String resolveKeyAnchorAndConvertToJson(String yaml, JSONUtils jsonUtils) throws UtilsException {
return resolveAnchorAndConvertToJson(yaml, jsonUtils, true);
}
private static String resolveAnchorAndConvertToJson(String yaml, JSONUtils jsonUtils, boolean resolveAllAnchor) throws UtilsException {
// Fix merge key '<<: *'
// La funzionalità di merge key è supportata fino allo yaml 1.1 (https://ktomk.github.io/writing/yaml-anchor-alias-and-merge-key.html)
// Mentre OpenAPI dice di usare preferibilmente YAML 1.2 (https://swagger.io/specification/):
// "n order to preserve the ability to round-trip between YAML and JSON formats, YAML version 1.2 is RECOMMENDED"
// Inoltre le anchor utilizzate nelle merge key non sono supportate correttamente in jackson:
// https://github.com/FasterXML/jackson-dataformats-text/issues/98
// Mentre vengono gestite correttamente da snake (https://linuxtut.com/convert-json-and-yaml-in-java-(using-jackson-and-snakeyaml)-0ad0a/)
// Come fix quindi nel caso siano presenti viene fatta una serializzazione tramite snake che le risolve.
boolean contains = resolveAllAnchor ? containsKeyAnchor(yaml) : containsMergeKeyAnchor(yaml);
if(contains) {
// Risoluzione merge key '<<: *'
Map<String, Object> obj = new org.yaml.snakeyaml.Yaml().load(yaml);
/**System.out.println("COSTRUITO ["+jsonUtils.toString(obj)+"]");*/
return jsonUtils.toString(obj); // jsonRepresentation
}
return null;
}
/** UTILS per tipi vuoi schema: {} */
// Fix: 1620
public static boolean containsEmptySchema(String yaml) {
String pattern = "(?m)^([ \\t]*)schema\\s*:\\s*\\{\\s*\\}\\s*$";
try {
Pattern p = Pattern.compile(pattern);
Matcher matcher = p.matcher(yaml);
return matcher.find();
}catch(Exception e) {
return false;
}
}
public static String resolveEmptySchema(String yaml) {
// Regex: trova righe con "schema: {}" e sostituisce con due righe correttamente indentate
return yaml.replaceAll("(?m)^([ \\t]*)schema:\\s*\\{\\s*\\}", "$1schema:\n$1 type: string");
}
// CONVERT TO MAP
public Map<String, Serializable> convertToMap(Logger log, String source, String raw) {
return this.convertToMap(log, source, raw, null);
}
public Map<String, Serializable> convertToMap(Logger log, String source, String raw, List<String> claimsToConvert) {
if(this.isYaml(raw)) {
return super.convertToMapEngine(log, source, raw, claimsToConvert);
}
else {
return new HashMap<>(); // empty return
}
}
public Map<String, Serializable> convertToMap(Logger log, String source, byte[]raw) {
return this.convertToMap(log, source, raw, null);
}
public Map<String, Serializable> convertToMap(Logger log, String source, byte[]raw, List<String> claimsToConvert) {
if(this.isYaml(raw)) {
return super.convertToMapEngine(log, source, raw, claimsToConvert);
}
else {
return new HashMap<>(); // empty return
}
}
}