001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.config.format; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020import static org.apache.juneau.commons.utils.StringUtils.*; 021 022import java.io.*; 023import java.util.*; 024 025import org.apache.juneau.config.internal.*; 026 027/** 028 * YAML config format. 029 */ 030public class YamlConfigFormat implements ConfigFormat { 031 032 /** Singleton instance. */ 033 public static final YamlConfigFormat INSTANCE = new YamlConfigFormat(); 034 035 /** 036 * Constructor. 037 */ 038 protected YamlConfigFormat() {} 039 040 @Override /* ConfigFormat */ 041 public String id() { 042 return "yaml"; 043 } 044 045 @Override /* ConfigFormat */ 046 @SuppressWarnings({ 047 "java:S135", // Multiple continue statements are intentional in this line-by-line YAML parsing state machine. 048 "java:S3776" // Cognitive complexity acceptable for YAML-to-INI format conversion logic 049 }) 050 public String toInternal(String contents) throws IOException { 051 if (contents == null) 052 return ""; 053 054 var lines = splitLines(contents); 055 var sb = new StringBuilder(); 056 var stack = new ArrayDeque<Node>(); 057 var preLines = new ArrayList<String>(); 058 String lastSection = null; 059 060 for (var i = 0; i < lines.size(); i++) { 061 var line = lines.get(i); 062 var trim = line.trim(); 063 if (trim.isEmpty() || trim.startsWith("#") || eq(trim, "---")) { 064 preLines.add(line); 065 continue; 066 } 067 068 var indent = leadingSpaces(line); 069 while (! stack.isEmpty() && indent <= stack.peekLast().indent) 070 stack.removeLast(); 071 072 var colonIndex = trim.indexOf(':'); 073 if (colonIndex <= 0) 074 throw ioex("Invalid YAML config line: %s", line); 075 076 var key = trim.substring(0, colonIndex).trim(); 077 var rest = trim.substring(colonIndex + 1).trim(); 078 if (eq(key, "_imports")) 079 continue; 080 081 if (rest.isEmpty()) { 082 stack.addLast(new Node(key, indent)); 083 continue; 084 } 085 086 String comment = null; 087 var commentIndex = rest.indexOf(" #"); 088 if (commentIndex != -1) { 089 comment = rest.substring(commentIndex + 2).trim(); 090 rest = rest.substring(0, commentIndex).trim(); 091 } 092 093 var value = unquote(rest); 094 var section = section(stack); 095 if (neq(section, lastSection)) { 096 appendPreLines(sb, preLines); 097 if (ine(section)) 098 sb.append('[').append(section).append(']').append('\n'); 099 lastSection = section; 100 } 101 appendPreLines(sb, preLines); 102 sb.append(key).append(" = ").append(value); 103 if (ine(comment)) 104 sb.append(" # ").append(comment); 105 sb.append('\n'); 106 } 107 appendPreLines(sb, preLines); 108 109 return sb.toString(); 110 } 111 112 @Override /* ConfigFormat */ 113 public String fromInternal(ConfigMap map) { 114 var sb = new StringBuilder(); 115 List<String> opened = l(); 116 for (var section : map.getSections()) { 117 var segments = splitPath(section); 118 var common = commonPrefix(opened, segments); 119 for (var i = common; i < segments.size(); i++) { 120 indent(sb, i).append(segments.get(i)).append(':').append('\n'); 121 } 122 opened = segments; 123 var keys = map.getKeys(section); 124 for (var key : keys) { 125 var entry = map.getEntry(section, key); 126 var value = entry == null ? "" : emptyIfNull(entry.getValue()); 127 indent(sb, segments.size()).append(key).append(": ").append(quoteIfNeeded(value)); 128 if (entry != null && ine(entry.getComment())) 129 sb.append(" # ").append(entry.getComment()); 130 sb.append('\n'); 131 } 132 } 133 return sb.toString(); 134 } 135 136 private int commonPrefix(List<String> a, List<String> b) { 137 var i = 0; 138 while (i < a.size() && i < b.size() && eq(a.get(i), b.get(i))) 139 i++; 140 return i; 141 } 142 143 private List<String> splitLines(String contents) throws IOException { 144 try (var r = new BufferedReader(new StringReader(contents))) { 145 var out = new ArrayList<String>(); 146 String line; 147 while ((line = r.readLine()) != null) 148 out.add(line); 149 return out; 150 } 151 } 152 153 private List<String> splitPath(String section) { 154 if (isEmpty(section)) 155 return Collections.emptyList(); 156 return Arrays.asList(section.split("/")); 157 } 158 159 private String section(Deque<Node> stack) { 160 if (stack.isEmpty()) 161 return ""; 162 var sb = new StringBuilder(); 163 for (var node : stack) { 164 if (! sb.isEmpty()) 165 sb.append('/'); 166 sb.append(node.name); 167 } 168 return sb.toString(); 169 } 170 171 private StringBuilder indent(StringBuilder sb, int level) { 172 for (var i = 0; i < level; i++) 173 sb.append(" "); 174 return sb; 175 } 176 177 private int leadingSpaces(String line) { 178 var i = 0; 179 while (i < line.length() && line.charAt(i) == ' ') 180 i++; 181 return i; 182 } 183 184 private void appendPreLines(StringBuilder sb, List<String> preLines) { 185 for (var line : preLines) 186 sb.append(line).append('\n'); 187 preLines.clear(); 188 } 189 190 private String unquote(String value) { 191 if (value.length() >= 2 && ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'")))) 192 return value.substring(1, value.length() - 1); 193 return value; 194 } 195 196 private String quoteIfNeeded(String value) { 197 if (value.isEmpty()) 198 return "\"\""; 199 if (value.indexOf(':') != -1 || value.indexOf('#') != -1 || value.startsWith(" ") || value.endsWith(" ")) 200 return '"' + value.replace("\"", "\\\"") + '"'; 201 return value; 202 } 203 204 private static class Node { 205 final String name; 206 final int indent; 207 208 Node(String name, int indent) { 209 this.name = name; 210 this.indent = indent; 211 } 212 } 213}