001package com.mrivanplays.annotationconfig.yaml;
002
003import com.mrivanplays.annotationconfig.core.resolver.MultilineString;
004import com.mrivanplays.annotationconfig.core.resolver.ValueWriter;
005import com.mrivanplays.annotationconfig.core.resolver.options.CustomOptions;
006import java.io.PrintWriter;
007import java.util.ArrayList;
008import java.util.Collections;
009import java.util.List;
010import java.util.Map;
011
012/**
013 * Represents the default yaml value writer. It uses homebrew writing to support comments.
014 *
015 * @author MrIvanPlays
016 * @since 2.1.0
017 */
018@SuppressWarnings("unchecked")
019public final class YamlValueWriter implements ValueWriter {
020
021  @Override
022  public void write(
023      Map<String, Object> values,
024      Map<String, List<String>> fieldComments,
025      PrintWriter writer,
026      CustomOptions options) {
027    for (Map.Entry<String, Object> entry : values.entrySet()) {
028      write(null, entry.getKey(), entry.getValue(), writer, fieldComments, 2, false, false);
029    }
030  }
031
032  private void write(
033      String parentKey,
034      String key,
035      Object value,
036      PrintWriter writer,
037      Map<String, List<String>> commentsMap,
038      int childIndents,
039      boolean child,
040      boolean additional2Spaces) {
041    StringBuilder intentPrefixBuilder = new StringBuilder();
042    for (int i = 0; i < childIndents; i++) {
043      intentPrefixBuilder.append(" ");
044    }
045    String intentPrefix = intentPrefixBuilder.toString();
046    String parentSpec = parentKey != null ? parentKey.concat("." + key) : key;
047    if (value instanceof Map<?, ?>) {
048      String firstCheck = parentKey != null ? parentKey + "." + key : key;
049      List<String> baseComments = commentsMap.getOrDefault(firstCheck, Collections.emptyList());
050      if (baseComments.isEmpty() && !firstCheck.equalsIgnoreCase(key)) {
051        baseComments = commentsMap.getOrDefault(key, Collections.emptyList());
052      }
053      String childPrefix = child ? intentPrefix.substring(0, childIndents - 2) : "";
054      if (!baseComments.isEmpty()) {
055        for (String comment : baseComments) {
056          writer.println(childPrefix + "# " + comment);
057        }
058      }
059      writer.println((additional2Spaces ? intentPrefix : childPrefix) + key + ":");
060      for (Map.Entry<String, Object> entry : ((Map<String, Object>) value).entrySet()) {
061        String mapKey = entry.getKey();
062        Object v = entry.getValue();
063        if (v instanceof List<?>) {
064          writeCommentsInsideMap(parentKey, key, writer, commentsMap, intentPrefix, mapKey);
065          List<?> vList = (List<?>) v;
066          if (vList.isEmpty()) {
067            writer.println(intentPrefix + mapKey + ": []");
068          } else {
069            boolean hasMapInsideMap = false;
070            OUT:
071            for (Object b : vList) {
072              if (b instanceof Map) {
073                Map<String, Object> map = (Map<String, Object>) b;
074                for (Map.Entry<String, Object> e1 : map.entrySet()) {
075                  if (e1.getValue() instanceof Map) {
076                    hasMapInsideMap = true;
077                    break OUT;
078                  }
079                }
080              }
081            }
082            if (!hasMapInsideMap) {
083              writer.println(intentPrefix + mapKey + ":");
084            }
085            for (Object b : vList) {
086              if (!(b instanceof String)) {
087                if (b instanceof Map) {
088                  Map<String, Object> map = (Map<String, Object>) b;
089                  boolean firstValue = true;
090                  List<Map.Entry<String, Object>> reservedObjectsForLater = new ArrayList<>();
091                  for (Map.Entry<String, Object> e : map.entrySet()) {
092                    if (e.getValue() instanceof Map || e.getValue() instanceof List) {
093                      reservedObjectsForLater.add(e);
094                      continue;
095                    }
096                    StringBuilder valueAppend = new StringBuilder();
097                    if (e.getValue() instanceof String) {
098                      valueAppend.append('"').append(e.getValue()).append('"');
099                    } else {
100                      valueAppend.append(e.getValue());
101                    }
102                    if (firstValue) {
103                      writer.println(intentPrefix + "- " + e.getKey() + ": " + valueAppend);
104                      firstValue = false;
105                    } else {
106                      writer.println(intentPrefix + "  " + e.getKey() + ": " + valueAppend);
107                    }
108                  }
109                  for (Map.Entry<String, Object> reserved : reservedObjectsForLater) {
110                    write(
111                        parentSpec,
112                        reserved.getKey(),
113                        reserved.getValue(),
114                        writer,
115                        commentsMap,
116                        childIndents + 2,
117                        true,
118                        true);
119                  }
120                } else {
121                  writer.println(intentPrefix + "    - " + b);
122                }
123              } else {
124                writer.println(intentPrefix + "    - \"" + b + "\"");
125              }
126            }
127          }
128        } else if (v instanceof Map<?, ?>) {
129          write(
130              parentSpec,
131              mapKey,
132              v,
133              writer,
134              commentsMap,
135              childIndents + 2,
136              true,
137              additional2Spaces);
138        } else {
139          writeCommentsInsideMap(parentKey, key, writer, commentsMap, intentPrefix, mapKey);
140          if (!(v instanceof String)) {
141            if (v instanceof MultilineString) {
142              MultilineString multiline = (MultilineString) v;
143              String toWrite = multiline.getString();
144              char c = multiline.getMarkerChar();
145              if (toWrite.indexOf('\n') != -1) {
146                String[] parts = toWrite.split("\n");
147                if (c == '|' || c == '>') {
148                  writer.println(intentPrefix + mapKey + ": " + c);
149                } else if (c != '"') {
150                  throw new IllegalArgumentException(
151                      "Invalid multiline string character '" + c + "' for YAML");
152                } else {
153                  writer.println(intentPrefix + mapKey + ": \"");
154                }
155                for (int i = 0; i < parts.length; i++) {
156                  String part = parts[i];
157                  if ((i + 1) == parts.length && c == '"') {
158                    writer.println(intentPrefix + part + "\"");
159                  } else {
160                    writer.println(intentPrefix + part + "\\n");
161                  }
162                }
163              } else {
164                writer.println(
165                    intentPrefix
166                        + (additional2Spaces ? "  " : "")
167                        + mapKey
168                        + ": \""
169                        + toWrite
170                        + "\"");
171              }
172            } else {
173              writer.println(intentPrefix + (additional2Spaces ? "  " : "") + mapKey + ": " + v);
174            }
175          } else {
176            writer.println(
177                intentPrefix + (additional2Spaces ? "  " : "") + mapKey + ": \"" + v + "\"");
178          }
179        }
180      }
181    } else if (value instanceof List<?>) {
182      writeComments(key, writer, commentsMap);
183      List<?> valueList = (List<?>) value;
184      if (valueList.isEmpty()) {
185        writer.println((child ? intentPrefix : "") + key + ": []");
186      } else {
187        boolean hasMapInsideMap = false;
188        OUT:
189        for (Object b : valueList) {
190          if (b instanceof Map) {
191            Map<String, Object> map = (Map<String, Object>) b;
192            for (Map.Entry<String, Object> e1 : map.entrySet()) {
193              if (e1.getValue() instanceof Map) {
194                hasMapInsideMap = true;
195                break OUT;
196              }
197            }
198          }
199        }
200        if (!hasMapInsideMap) {
201          writer.println((child ? intentPrefix : "") + key + ":");
202        }
203        for (Object b : valueList) {
204          if (!(b instanceof String)) {
205            if (b instanceof Map) {
206              Map<String, Object> map = (Map<String, Object>) b;
207              boolean firstValue = true;
208              List<Map.Entry<String, Object>> reservedObjectsForLater = new ArrayList<>();
209              for (Map.Entry<String, Object> e : map.entrySet()) {
210                if (e.getValue() instanceof Map || e.getValue() instanceof List) {
211                  reservedObjectsForLater.add(e);
212                  continue;
213                }
214                StringBuilder valueAppend = new StringBuilder();
215                if (e.getValue() instanceof String) {
216                  valueAppend.append('"').append(e.getValue()).append('"');
217                } else {
218                  valueAppend.append(e.getValue());
219                }
220                if (firstValue) {
221                  writer.println(intentPrefix + "- " + e.getKey() + ": " + valueAppend);
222                  firstValue = false;
223                } else {
224                  writer.println(intentPrefix + "  " + e.getKey() + ": " + valueAppend);
225                }
226              }
227              for (Map.Entry<String, Object> reserved : reservedObjectsForLater) {
228                write(
229                    parentSpec,
230                    reserved.getKey(),
231                    reserved.getValue(),
232                    writer,
233                    commentsMap,
234                    childIndents + 2,
235                    true,
236                    true);
237              }
238            } else {
239              writer.println(intentPrefix + "- " + b);
240            }
241          } else {
242            writer.println(intentPrefix + "- \"" + b + "\"");
243          }
244        }
245      }
246    } else {
247      writeComments(key, writer, commentsMap);
248      if (!(value instanceof String)) {
249        if (value instanceof MultilineString) {
250          MultilineString multiline = (MultilineString) value;
251          String toWrite = multiline.getString();
252          char c = multiline.getMarkerChar();
253          if (toWrite.indexOf('\n') != -1) {
254            String[] parts = toWrite.split("\n");
255            if (c == '|' || c == '>') {
256              writer.println((child ? intentPrefix : "") + key + ": " + c);
257            } else if (c != '"') {
258              throw new IllegalArgumentException(
259                  "Invalid multiline string character '" + c + "' for YAML");
260            } else {
261              writer.println((child ? intentPrefix : "") + key + ": \"");
262            }
263            for (int i = 0; i < parts.length; i++) {
264              String part = parts[i];
265              if ((i + 1) == parts.length && c == '"') {
266                writer.println((child ? intentPrefix : "") + part + "\"");
267              } else {
268                writer.println((child ? intentPrefix : "") + part + "\\n");
269              }
270            }
271          } else {
272            writer.println((child ? intentPrefix : "") + key + ": \"" + toWrite + "\"");
273          }
274        } else {
275          writer.println((child ? intentPrefix : "") + key + ": " + value);
276        }
277      } else {
278        writer.println((child ? intentPrefix : "") + key + ": \"" + value + "\"");
279      }
280    }
281    if (!child) {
282      writer.append('\n');
283    }
284  }
285
286  private void writeComments(
287      String key, PrintWriter writer, Map<String, List<String>> commentsMap) {
288    List<String> comments = commentsMap.getOrDefault(key, Collections.emptyList());
289    if (!comments.isEmpty()) {
290      for (String comment : comments) {
291        writer.println("# " + comment);
292      }
293    }
294  }
295
296  private void writeCommentsInsideMap(
297      String parentKey,
298      String key,
299      PrintWriter writer,
300      Map<String, List<String>> commentsMap,
301      String intentPrefix,
302      String mapKey) {
303    List<String> comments =
304        commentsMap.getOrDefault(
305            (parentKey != null ? parentKey + "." : "") + key + "." + mapKey,
306            Collections.emptyList());
307    if (!comments.isEmpty()) {
308      for (String comment : comments) {
309        writer.println(intentPrefix + "# " + comment);
310      }
311    }
312  }
313}