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.*;
026
027/**
028 * Represents a single entry in a configuration.
029 *
030 * This is a read-only object.
031 *
032 * @param rawLine The raw line this entry was parsed from, or <jk>null</jk> if the entry was created programmatically.
033 * @param key The name of this entry.
034 * @param value The raw value of this entry.
035 * @param comment The same-line comment of this entry, or <jk>null</jk> if it has no comment.
036 * @param modifiers The modifiers for this entry, or <jk>null</jk> if it has no modifiers.
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, String modifiers, List<String> preLines) {
040
041   static final ConfigMapEntry NULL = new ConfigMapEntry(null, 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 modifiers The modifiers for this entry, or <jk>null</jk> if it has no modifiers.
060    * @param comment The same-line comment of this entry, or <jk>null</jk> if it has no comment.
061    * @param preLines The pre-lines of this entry.  Can be <jk>null</jk> (treated as an empty list).
062    */
063   ConfigMapEntry(String key, String value, String modifiers, String comment, List<String> preLines) {
064      this(null, key, value, comment, modifiers, preLines);
065   }
066
067   /**
068    * Parses a raw config line into an entry.
069    *
070    * @param line The raw line to parse.
071    * @param preLines The pre-lines preceding this entry.
072    * @return A new immutable entry.
073    */
074   static ConfigMapEntry parse(String line, List<String> preLines) {
075      var i = line.indexOf('=');
076      var key2 = line.substring(0, i).trim();
077
078      var m1 = key2.indexOf('<');
079      var m2 = key2.indexOf('>');
080
081      var modifiers = nie((m1 > -1 && m2 > m1) ? key2.substring(m1 + 1, m2) : null);
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, modifiers, 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 modifiers for this entry.
115    *
116    * @return The modifiers for this entry, or <jk>null</jk> if it has no modifiers.
117    */
118   public String getModifiers() { return modifiers; }
119
120   /**
121    * Returns the pre-lines of this entry.
122    *
123    * @return The pre-lines of this entry as an unmodifiable list.
124    */
125   public List<String> getPreLines() { return preLines; }
126
127   /**
128    * Returns the raw value of this entry.
129    *
130    * @return The raw value of this entry.
131    */
132   public String getValue() { return value; }
133
134   @SuppressWarnings({
135      "java:S3776" // Cognitive complexity acceptable for config entry writer
136   })
137   Writer writeTo(Writer w) throws IOException {
138      if (value == null)
139         return w;
140      for (var pl : preLines)
141         w.append(pl).append('\n');
142      if (nn(rawLine)) {
143         for (var i = 0; i < rawLine.length(); i++) {
144            var c = rawLine.charAt(i);
145            if (c == '\n')
146               w.append('\n').append('\t');
147            else if (c != '\r') // HTT - '\r' in raw line requires OS-level carriage return in stored config content
148               w.append(c);
149         }
150         w.append('\n');
151      } else {
152         w.append(key);
153         if (nn(modifiers))
154            w.append('<').append(modifiers).append('>');
155         w.append(" = ");
156
157         var val = value;
158         for (var i = 0; i < val.length(); i++) {
159            var c = val.charAt(i);
160            if (c == '\n')
161               w.append('\n').append('\t');
162            else if (c != '\r') { // HTT - '\r' in value requires OS-level carriage return injection
163               if (REPLACE_CHARS.contains(c) || (Character.isISOControl(c) && c != '\t')) { // c is already known to be neither '\n' nor '\r' here
164                  w.append(unicodeSequence(c));
165               } else {
166                  w.append(c);
167               }
168            }
169         }
170
171         if (ine(comment))
172            w.append(" # ").append(comment);
173
174         w.append('\n');
175      }
176      return w;
177   }
178}