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}