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.*; 020 021import java.io.*; 022 023import org.apache.juneau.commons.bean.*; 024import org.apache.juneau.commons.collections.*; 025import org.apache.juneau.marshall.*; 026import org.apache.juneau.marshall.parser.*; 027import org.apache.juneau.marshall.stream.*; 028 029/** 030 * Parses HOCON (Human-Optimized Config Object Notation) text into POJO models. 031 * 032 * <h5 class='topic'>Media types</h5> 033 * <p> 034 * Handles <c>Content-Type</c> types: <bc>application/hocon</bc> 035 * 036 * <h5 class='topic'>Description</h5> 037 * <p> 038 * Multi-pass parsing strategy: tokenize → build intermediate tree → resolve substitutions → convert to bean. 039 * 040 * <p> 041 * Accepts all three key-value separators: <c>=</c> (HOCON convention), <c>:</c> (JSON style), and whitespace before <c>{</c> (for object values). 042 * 043 * <p> 044 * Supports unquoted string keys and values, path expressions (<c>a.b.c = value</c>), object merging, substitution resolution, 045 * triple-quoted strings, both <c>//</c> and <c>#</c> comments, and optional root braces. 046 * 047 * <h5 class='figure'>Example input (bean):</h5> 048 * <p class='bjson'> 049 * name = myapp 050 * port = 8080 051 * debug = true 052 * database { 053 * host = localhost 054 * port = 5432 055 * } 056 * tags = [web, api, rest] 057 * </p> 058 * 059 * <h5 class='section'>Limitations:</h5><ul> 060 * <li class='note'><c>include</c> directives are not supported — they are ignored with a warning during parsing. 061 * <li class='note'>Duration/size unit suffixes (<c>10s</c>, <c>5m</c>, <c>512K</c>, <c>2G</c>) are treated as unquoted strings. 062 * <li class='note'>Substitutions are resolved before bean conversion; the resolved (concrete) values are what Juneau sees. 063 * </ul> 064 * 065 * <h5 class='section'>See Also:</h5><ul> 066 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/HoconSupport">HOCON</a> 067 * </ul> 068 */ 069@SuppressWarnings({ 070 "java:S110", // Depth comes from the shared Parser -> ReaderParser base hierarchy 071 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 072 "java:S9149", // Per-format static factories intentionally shadow the parent's. 073 "resource" // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design. 074}) 075public class HoconParser extends ReaderParser implements HoconMetaProvider, RecordReadable { 076 077 private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>,HoconClassMeta> hoconClassMetas = new java.util.concurrent.ConcurrentHashMap<>(); 078 private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta,HoconBeanPropertyMeta> hoconBeanPropertyMetas = new java.util.concurrent.ConcurrentHashMap<>(); 079 080 /** Whether to resolve ${var} substitutions. */ 081 protected final boolean resolveSubstitutions; 082 083 /** 084 * Builder class. 085 */ 086 public static class Builder extends ReaderParser.Builder<Builder> { 087 088 private static final Cache<HashKey,HoconParser> CACHE = Cache.of(HashKey.class, HoconParser.class).build(); 089 090 private boolean resolveSubstitutions = true; 091 092 protected Builder() { 093 consumes("application/hocon"); 094 } 095 096 protected Builder(Builder copyFrom) { 097 super(reqnn("ctx", copyFrom)); 098 resolveSubstitutions = copyFrom.resolveSubstitutions; 099 } 100 101 protected Builder(HoconParser copyFrom) { 102 super(reqnn("ctx", copyFrom)); 103 resolveSubstitutions = copyFrom.resolveSubstitutions; 104 } 105 106 /** 107 * Enables substitution resolution. 108 * 109 * @param value <jk>true</jk> to resolve ${var} and ${?var}. 110 * @return This object. 111 */ 112 public Builder resolveSubstitutions(boolean value) { 113 resolveSubstitutions = value; 114 return this; 115 } 116 117 @Override 118 public HoconParser build() { 119 return cache(CACHE).build(HoconParser.class); 120 } 121 122 @Override 123 public Builder copy() { 124 return new Builder(this); 125 } 126 127 @Override 128 public HashKey hashKey() { 129 return HashKey.of(super.hashKey(), resolveSubstitutions); 130 } 131 } 132 133 /** Default parser. */ 134 public static final HoconParser DEFAULT = new HoconParser(create()); 135 136 /** 137 * Creates a new builder. 138 * 139 * @return A new builder. 140 */ 141 public static Builder create() { 142 return new Builder(); 143 } 144 145 /** 146 * Constructor. 147 * 148 * @param builder The builder. 149 */ 150 public HoconParser(Builder builder) { 151 super(builder); 152 resolveSubstitutions = builder.resolveSubstitutions; 153 } 154 155 @Override 156 public HoconParserSession.Builder createSession() { 157 return HoconParserSession.create(this); 158 } 159 160 @Override 161 public Builder copy() { 162 return new Builder(this); 163 } 164 165 @Override 166 public HoconBeanPropertyMeta getHoconBeanPropertyMeta(BeanPropertyMeta bpm) { 167 if (bpm == null) 168 return HoconBeanPropertyMeta.DEFAULT; 169 return hoconBeanPropertyMetas.computeIfAbsent(bpm, k -> new HoconBeanPropertyMeta(k, this)); 170 } 171 172 @Override 173 public HoconClassMeta getHoconClassMeta(ClassMeta<?> cm) { 174 return hoconClassMetas.computeIfAbsent(cm, k -> new HoconClassMeta(k, this)); 175 } 176 177 /** 178 * Convenience delegator that opens a {@link RecordReader} over the input using 179 * <b>default session arguments</b> (mirrors {@link #read(Object, Class)}). 180 * 181 * <p> 182 * The real implementation lives on {@link HoconParserSession#readRecords(Object)}. Callers 183 * that need request-derived configuration (locale, timezone, schema, swaps) should call 184 * {@link #createSession()} and invoke {@link HoconParserSession#readRecords(Object)} on the 185 * built session instead. 186 * 187 * @param input The input. 188 * @return A new {@link RecordReader} cursor. 189 * @throws IOException If a problem occurred opening the underlying input. 190 */ 191 @Override /* RecordReadable */ 192 public RecordReader readRecords(Object input) throws IOException { 193 return ((RecordReadable) getSession()).readRecords(input); 194 } 195 196 @Override /* RecordReadable */ 197 public boolean isRecordStreaming() { 198 return false; 199 } 200}