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.internal;
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.commons.lang.*;
026import org.apache.juneau.marshall.*;
027
028/**
029 * Represents a single entry in a configuration.
030 *
031 * This is a read-only object.
032 *
033 * @param rawLine The raw line this entry was parsed from, or <jk>null</jk> if the entry was created programmatically.
034 * @param key The name of this entry.
035 * @param value The raw value of this entry.
036 * @param comment The same-line comment of this entry, or <jk>null</jk> if it has no comment.
037 * @param preLines The pre-lines of this entry as an unmodifiable list.
038 */
039public record ConfigMapEntry(String rawLine, String key, String value, String comment, List<String> preLines) {
040
041   static final ConfigMapEntry NULL = new ConfigMapEntry(null, null, null, null, null);
042   private static final AsciiSet REPLACE_CHARS = AsciiSet.of("\\#");
043
044   /**
045    * Canonical constructor.
046    *
047    * <p>
048    * Normalizes {@code preLines} to an unmodifiable copy (empty when <jk>null</jk>) so this record is truly immutable.
049    */
050   public ConfigMapEntry {
051      preLines = preLines == null ? Collections.emptyList() : u(cp(preLines));
052   }
053
054   /**
055    * Constructor for a programmatically-created entry (no raw line).
056    *
057    * @param key The name of this entry.
058    * @param value The raw value of this entry.
059    * @param comment The same-line comment of this entry, or <jk>null</jk> if it has no comment.
060    * @param preLines The pre-lines of this entry.  Can be <jk>null</jk> (treated as an empty list).
061    */
062   ConfigMapEntry(String key, String value, String comment, List<String> preLines) {
063      this(null, key, value, comment, preLines);
064   }
065
066   /**
067    * Parses a raw config line into an entry.
068    *
069    * @param line The raw line to parse.
070    * @param preLines The pre-lines preceding this entry.
071    * @return A new immutable entry.
072    */
073   static ConfigMapEntry parse(String line, List<String> preLines) {
074      var i = line.indexOf('=');
075      var key2 = line.substring(0, i).trim();
076
077      var m1 = key2.indexOf('<');
078      var m2 = key2.indexOf('>');
079      if (m1 > -1 && m2 > m1)
080         throw new ConfigException("Entry modifiers were removed in 10.0 (found on entry '%s'); use ${SEC:...} for secrets instead.", key2.substring(0, m1).trim());
081
082      var key = m1 == -1 ? key2 : key2.substring(0, m1);
083
084      var rest = line.substring(i + 1);
085
086      String comment = null;
087      if (rest.indexOf('#') != -1) {
088         var l2 = splita(rest, '#', 2);
089         rest = l2[0];
090         if (l2.length == 2)
091            comment = l2[1].trim();
092      }
093
094      var value = replaceUnicodeSequences(rest.trim());
095
096      return new ConfigMapEntry(line, key, value, comment, preLines);
097   }
098
099   /**
100    * Returns the same-line comment of this entry.
101    *
102    * @return The same-line comment of this entry, or <jk>null</jk> if it has no comment.
103    */
104   public String getComment() { return comment; }
105
106   /**
107    * Returns the name of this entry.
108    *
109    * @return The name of this entry.
110    */
111   public String getKey() { return key; }
112
113   /**
114    * Returns the pre-lines of this entry.
115    *
116    * @return The pre-lines of this entry as an unmodifiable list.
117    */
118   public List<String> getPreLines() { return preLines; }
119
120   /**
121    * Returns the raw value of this entry.
122    *
123    * @return The raw value of this entry.
124    */
125   public String getValue() { return value; }
126
127   @SuppressWarnings({
128      "java:S3776" // Cognitive complexity acceptable for config entry writer
129   })
130   Writer writeTo(Writer w) throws IOException {
131      if (value == null)
132         return w;
133      for (var pl : preLines)
134         w.append(pl).append('\n');
135      if (nn(rawLine)) {
136         for (var i = 0; i < rawLine.length(); i++) {
137            var c = rawLine.charAt(i);
138            if (c == '\n')
139               w.append('\n').append('\t');
140            else if (c != '\r') // HTT - '\r' in raw line requires OS-level carriage return in stored config content
141               w.append(c);
142         }
143         w.append('\n');
144      } else {
145         w.append(key);
146         w.append(" = ");
147
148         var val = value;
149         for (var i = 0; i < val.length(); i++) {
150            var c = val.charAt(i);
151            if (c == '\n')
152               w.append('\n').append('\t');
153            else if (c != '\r') { // HTT - '\r' in value requires OS-level carriage return injection
154               if (REPLACE_CHARS.contains(c) || (Character.isISOControl(c) && c != '\t')) { // c is already known to be neither '\n' nor '\r' here
155                  w.append(unicodeSequence(c));
156               } else {
157                  w.append(c);
158               }
159            }
160         }
161
162         if (ine(comment))
163            w.append(" # ").append(comment);
164
165         w.append('\n');
166      }
167      return w;
168   }
169}