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.marshall.hocon;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.commons.utils.StringUtils.*;
021
022import java.util.*;
023
024/**
025 * Intermediate representation for parsed HOCON data before conversion to Java beans.
026 *
027 * <p>
028 * HOCON parsing is multi-pass: tokenize → build tree (with path expressions and object merging) → resolve substitutions → convert to bean.
029 * This tree allows handling of path expressions ({@code a.b.c = value}), object merging (duplicate keys merge),
030 * value concatenation, and substitution resolution.
031 *
032 * <h5 class='section'>See Also:</h5><ul>
033 *    <li class='link'><a class="doclink" href="https://github.com/lightbend/config/blob/main/HOCON.md">HOCON Specification</a>
034 * </ul>
035 */
036public abstract sealed class HoconValue {
037
038   /** Value type. */
039   public enum Type {
040      /** String value. */
041      STRING,
042      /** Numeric value. */
043      NUMBER,
044      /** Boolean value. */
045      BOOLEAN,
046      /** Null value. */
047      NULL,
048      /** Object (map/bean). */
049      OBJECT,
050      /** Array (list). */
051      ARRAY,
052      /** Substitution reference (${var}). */
053      SUBSTITUTION,
054      /** Optional substitution reference (${?var}). */
055      OPT_SUBSTITUTION,
056      /** Concatenation of adjacent values. */
057      CONCAT
058   }
059
060   /**
061    * Returns the value type.
062    *
063    * @return The type.
064    */
065   public abstract Type getType();
066
067   /**
068    * HOCON object (maps to Java bean or Map).
069    */
070   public static final class HoconObject extends HoconValue {
071      private final LinkedHashMap<String,HoconValue> members = new LinkedHashMap<>();
072
073      @Override
074      public Type getType() {
075         return Type.OBJECT;
076      }
077
078      /**
079       * Returns the members.
080       *
081       * @return The members map.
082       */
083      @SuppressWarnings({
084         "java:S1319" // LinkedHashMap return type intentional; HOCON preserves insertion order of object members
085      })
086      public LinkedHashMap<String,HoconValue> getMembers() {
087         return members;
088      }
089
090      /**
091       * Merges another object into this one (HOCON object merging).
092       *
093       * @param other The other object to merge.
094       *    <br>Must not be <jk>null</jk>.
095       */
096      public void merge(HoconObject other) {
097         for (var e : other.members.entrySet()) {
098            var key = e.getKey();
099            var existing = members.get(key);
100            var incoming = e.getValue();
101            if (existing instanceof HoconObject existing2 && incoming instanceof HoconObject incoming2)
102               existing2.merge(incoming2);
103            else
104               members.put(key, incoming);
105         }
106      }
107
108      /**
109       * Sets a value at a dotted path (e.g., a.b.c).
110       *
111       * @param path Path components.
112       *    <br>Can be <jk>null</jk> or empty (no-op).
113       * @param value The value.
114       */
115      public void setPath(String[] path, HoconValue value) {
116         if (path == null || path.length == 0)
117            return;
118         if (path.length == 1) {
119            members.put(path[0], value);
120            return;
121         }
122         var first = path[0];
123         var existing = members.get(first);
124         HoconObject sub;
125         if (existing instanceof HoconObject existing2) {
126            sub = existing2;
127         } else {
128            sub = new HoconObject();
129            members.put(first, sub);
130         }
131         sub.setPath(Arrays.copyOfRange(path, 1, path.length), value);
132      }
133
134      /**
135       * Gets a value at a dotted path.
136       *
137       * @param path Path components.
138       *    <br>Can be <jk>null</jk> or empty (returns <jk>null</jk>).
139       * @return The value, or <jk>null</jk> if the path is <jk>null</jk>, empty, or does not resolve to an existing
140       *    value.
141       */
142      public HoconValue getPath(String[] path) {
143         if (path == null || path.length == 0)
144            return null;
145         var val = members.get(path[0]);
146         if (path.length == 1)
147            return val;
148         if (val instanceof HoconObject val2)
149            return val2.getPath(Arrays.copyOfRange(path, 1, path.length));
150         return null;
151      }
152   }
153
154   /**
155    * HOCON array (maps to List or array).
156    */
157   public static final class HoconArray extends HoconValue {
158      private final List<HoconValue> elements = l();
159
160      @Override
161      public Type getType() {
162         return Type.ARRAY;
163      }
164
165      /**
166       * Returns the elements.
167       *
168       * @return An unmodifiable copy of the elements list.
169       */
170      public List<HoconValue> getElements() {
171         return u(cp(elements));
172      }
173
174      /**
175       * Adds an element to this array.
176       *
177       * @param element The element to add.
178       */
179      public void add(HoconValue element) {
180         elements.add(element);
181      }
182
183      /**
184       * Replaces the contents of this array with the specified elements.
185       *
186       * @param value The new elements. Can be <jk>null</jk> to clear the array.
187       */
188      public void setElements(List<HoconValue> value) {
189         elements.clear();
190         if (value != null)
191            elements.addAll(value);
192      }
193
194      /**
195       * Concatenates another array (HOCON array concatenation).
196       *
197       * @param other The other array.
198       *    <br>Must not be <jk>null</jk>.
199       */
200      public void concat(HoconArray other) {
201         elements.addAll(other.elements);
202      }
203   }
204
205   /**
206    * HOCON string value.
207    */
208   public static final class HoconString extends HoconValue {
209      private final String value;
210
211      /**
212       * Constructor.
213       *
214       * @param value The string value.
215       */
216      public HoconString(String value) {
217         this.value = value;
218      }
219
220      @Override
221      public Type getType() {
222         return Type.STRING;
223      }
224
225      /**
226       * Returns the string value.
227       *
228       * @return The value.
229       */
230      public String getValue() {
231         return value;
232      }
233   }
234
235   /**
236    * HOCON number value.
237    */
238   public static final class HoconNumber extends HoconValue {
239      private final Number value;
240
241      /**
242       * Constructor.
243       *
244       * @param value The number value.
245       */
246      public HoconNumber(Number value) {
247         this.value = value;
248      }
249
250      @Override
251      public Type getType() {
252         return Type.NUMBER;
253      }
254
255      /**
256       * Returns the number value.
257       *
258       * @return The value.
259       */
260      public Number getValue() {
261         return value;
262      }
263   }
264
265   /**
266    * HOCON boolean value.
267    */
268   public static final class HoconBoolean extends HoconValue {
269      private final boolean value;
270
271      /**
272       * Constructor.
273       *
274       * @param value The boolean value.
275       */
276      public HoconBoolean(boolean value) {
277         this.value = value;
278      }
279
280      @Override
281      public Type getType() {
282         return Type.BOOLEAN;
283      }
284
285      /**
286       * Returns the boolean value.
287       *
288       * @return The value.
289       */
290      public boolean getValue() {
291         return value;
292      }
293   }
294
295   /**
296    * HOCON null value.
297    */
298   public static final class HoconNull extends HoconValue {
299      @Override
300      public Type getType() {
301         return Type.NULL;
302      }
303   }
304
305   /**
306    * HOCON substitution reference (${var} or ${?var}).
307    */
308   public static final class HoconSubstitution extends HoconValue {
309      private final String path;
310      private final boolean optional;
311
312      /**
313       * Constructor.
314       *
315       * @param path The reference path.
316       * @param optional Whether optional (${?var}).
317       */
318      public HoconSubstitution(String path, boolean optional) {
319         this.path = path;
320         this.optional = optional;
321      }
322
323      @Override
324      public Type getType() {
325         return optional ? Type.OPT_SUBSTITUTION : Type.SUBSTITUTION;
326      }
327
328      /**
329       * Returns the reference path.
330       *
331       * @return The path.
332       */
333      public String getPath() {
334         return path;
335      }
336
337      /**
338       * Returns whether this is an optional substitution.
339       *
340       * @return <jk>true</jk> for ${?var}.
341       */
342      public boolean isOptional() {
343         return optional;
344      }
345   }
346
347   /**
348    * HOCON value concatenation (adjacent values concatenated).
349    */
350   public static final class HoconConcat extends HoconValue {
351      private final List<HoconValue> parts = l();
352
353      @Override
354      public Type getType() {
355         return Type.CONCAT;
356      }
357
358      /**
359       * Returns the concatenated parts.
360       *
361       * @return The parts list.
362       */
363      public List<HoconValue> getParts() {
364         return u(cp(parts));
365      }
366
367      /**
368       * Adds a part.
369       *
370       * @param part The part to add.
371       */
372      public void add(HoconValue part) {
373         parts.add(part);
374      }
375
376      /**
377       * Returns whether this concat contains a substitution for the given path.
378       *
379       * @param path The dotted path (e.g., "path" or "a.b.c").
380       *    <br>Can be <jk>null</jk> or empty (returns <jk>false</jk>).
381       * @return <jk>true</jk> if any part is a substitution for that path.
382       */
383      public boolean referencesPath(String path) {
384         if (isEmpty(path))
385            return false;
386         for (var p : parts) {
387            if (p instanceof HoconSubstitution p2 && path.equals(p2.getPath()))
388               return true;
389         }
390         return false;
391      }
392   }
393}