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