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; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.beans.*; 022import java.lang.reflect.*; 023import java.util.*; 024 025import org.apache.juneau.config.internal.*; 026import org.apache.juneau.marshall.collections.*; 027import org.apache.juneau.marshall.parser.*; 028 029/** 030 * A single section in a config file. 031 */ 032@SuppressWarnings({ 033 "java:S1192" // Duplicated literals (argument/property names) read more clearly inline than as constants 034}) 035public class Section { 036 037 final Config config; 038 private final ConfigMap configMap; 039 final String name; 040 041 /** 042 * Constructor. 043 * 044 * @param config The config that this entry belongs to. 045 * @param configMap The map that this belongs to. 046 * @param name The section name of this entry. 047 */ 048 protected Section(Config config, ConfigMap configMap, String name) { 049 this.config = config; 050 this.configMap = configMap; 051 this.name = name; 052 } 053 054 /** 055 * Shortcut for calling <code>asBean(sectionName, c, <jk>false</jk>)</code>. 056 * 057 * @param <T> The bean class to create. 058 * @param c 059 * The bean class to create. 060 * <br>Must not be <jk>null</jk>. 061 * @return A new bean instance, or {@link Optional#empty()} if this section does not exist. 062 * @throws ParseException Malformed input encountered. 063 */ 064 public <T> Optional<T> asBean(Class<T> c) throws ParseException { 065 return asBean(c, false); 066 } 067 068 /** 069 * Converts this config file section to the specified bean instance. 070 * 071 * <p> 072 * Key/value pairs in the config file section get copied as bean property values to the specified bean class. 073 * 074 * <h5 class='figure'>Example config file</h5> 075 * <p class='bini'> 076 * <cs>[MyAddress]</cs> 077 * <ck>name</ck> = <cv>John Smith</cv> 078 * <ck>street</ck> = <cv>123 Main Street</cv> 079 * <ck>city</ck> = <cv>Anywhere</cv> 080 * <ck>state</ck> = <cv>NY</cv> 081 * <ck>zip</ck> = <cv>12345</cv> 082 * </p> 083 * 084 * <h5 class='figure'>Example bean</h5> 085 * <p class='bjava'> 086 * <jk>public class</jk> Address { 087 * <jk>public</jk> String <jf>name</jf>, <jf>street</jf>, <jf>city</jf>; 088 * <jk>public</jk> StateEnum <jf>state</jf>; 089 * <jk>public int</jk> <jf>zip</jf>; 090 * } 091 * </p> 092 * 093 * <h5 class='figure'>Example usage</h5> 094 * <p class='bjava'> 095 * Config <jv>config</jv> = Config.<jsm>create</jsm>().name(<js>"MyConfig.cfg"</js>).build(); 096 * Address <jv>address</jv> = <jv>config</jv>.getSection(<js>"MySection"</js>).asBean(Address.<jk>class</jk>).orElse(<jk>null</jk>); 097 * </p> 098 * 099 * @param <T> The bean class to create. 100 * @param c 101 * The bean class to create. 102 * <br>Must not be <jk>null</jk>. 103 * @param ignoreUnknownProperties 104 * If <jk>false</jk>, throws a {@link ParseException} if the section contains an entry that isn't a bean property 105 * name. 106 * @return A new bean instance, or an empty {@link Optional} if this section doesn't exist. 107 * @throws ParseException Unknown property was encountered in section. 108 */ 109 public <T> Optional<T> asBean(Class<T> c, boolean ignoreUnknownProperties) throws ParseException { 110 reqnn("c", c); 111 112 if (! isPresent()) 113 return oe(); 114 115 var keys = configMap.getKeys(name); 116 117 var bm = config.marshallingSession.newBeanMap(c); 118 for (var k : keys) { 119 var bpm = bm.getPropertyMeta(k); 120 if (bpm == null) { 121 if (! ignoreUnknownProperties) 122 throw new ParseException("Unknown property '%s' encountered in configuration section '%s'.", k, name); 123 } else { 124 bm.put(k, config.get(name + '/' + k).as(bpm.getBeanInfo().inner()).orElse(null)); 125 } 126 } 127 128 return o(bm.getBean()); 129 } 130 131 /** 132 * Wraps this section inside a Java interface so that values in the section can be read and 133 * write using getters and setters. 134 * 135 * <h5 class='figure'>Example config file</h5> 136 * <p class='bini'> 137 * <cs>[MySection]</cs> 138 * <ck>string</ck> = <cv>foo</cv> 139 * <ck>int</ck> = <cv>123</cv> 140 * <ck>enum</ck> = <cv>ONE</cv> 141 * <ck>bean</ck> = <cv>{foo:'bar',baz:123}</cv> 142 * <ck>int3dArray</ck> = <cv>[[[123,null],null],null]</cv> 143 * <ck>bean1d3dListMap</ck> = <cv>{key:[[[[{foo:'bar',baz:123}]]]]}</cv> 144 * </p> 145 * 146 * <h5 class='figure'>Example interface</h5> 147 * <p class='bjava'> 148 * <jk>public interface</jk> MyConfigInterface { 149 * 150 * String getString(); 151 * <jk>void</jk> setString(String <jv>value</jv>); 152 * 153 * <jk>int</jk> getInt(); 154 * <jk>void</jk> setInt(<jk>int</jk> <jv>value</jv>); 155 * 156 * MyEnum getEnum(); 157 * <jk>void</jk> setEnum(MyEnum <jv>value</jv>); 158 * 159 * MyBean getBean(); 160 * <jk>void</jk> setBean(MyBean <jv>value</jv>); 161 * 162 * <jk>int</jk>[][][] getInt3dArray(); 163 * <jk>void</jk> setInt3dArray(<jk>int</jk>[][][] <jv>value</jv>); 164 * 165 * Map<String,List<MyBean[][][]>> getBean1d3dListMap(); 166 * <jk>void</jk> setBean1d3dListMap(Map<String,List<MyBean[][][]>> <jv>value</jv>); 167 * } 168 * </p> 169 * 170 * <h5 class='figure'>Example usage</h5> 171 * <p class='bjava'> 172 * Config <jv>config</jv> = Config.<jsm>create</jsm>().name(<js>"MyConfig.cfg"</js>).build(); 173 * 174 * MyConfigInterface <jv>ci</jv> = <jv>config</jv>.get(<js>"MySection"</js>).asInterface(MyConfigInterface.<jk>class</jk>).orElse(<jk>null</jk>); 175 * 176 * <jk>int</jk> <jv>myInt</jv> = <jv>ci</jv>.getInt(); 177 * 178 * <jv>ci</jv>.setBean(<jk>new</jk> MyBean()); 179 * 180 * <jv>ci</jv>.save(); 181 * </p> 182 * 183 * <h5 class='section'>Notes:</h5><ul> 184 * <li class='note'>Calls to setters when the configuration is read-only will cause {@link UnsupportedOperationException} to be thrown. 185 * </ul> 186 * 187 * @param <T> The proxy interface class. 188 * @param c 189 * The proxy interface class. 190 * <br>Must not be <jk>null</jk>. 191 * @return The proxy interface. 192 * @throws IllegalArgumentException If the class is not an interface. 193 */ 194 @SuppressWarnings({ 195 "unchecked" // Type erasure requires cast to T for interface proxy 196 }) 197 public <T> Optional<T> asInterface(Class<T> c) { 198 reqnn("c", c); 199 200 if (! c.isInterface()) 201 throw iaex("Class '%s' passed to toInterface() is not an interface.", cn(c)); 202 203 return o((T)Proxy.newProxyInstance(c.getClassLoader(), a(c), (InvocationHandler)(proxy, method, args) -> { 204 var bi = Introspector.getBeanInfo(c, null); 205 for (var pd : bi.getPropertyDescriptors()) { 206 var rm = pd.getReadMethod(); 207 var wm = pd.getWriteMethod(); 208 if (method.equals(rm)) 209 return config.get(name + '/' + pd.getName()).as(rm.getGenericReturnType()).orElse(null); 210 if (method.equals(wm)) 211 return config.set(name + '/' + pd.getName(), args[0]); 212 } 213 throw uoex("Unsupported interface method. method='%s'", method); 214 })); 215 } 216 217 /** 218 * Returns this section as a map. 219 * 220 * @return A new {@link JsonMap}, or {@link Optional#empty()} if this section doesn't exist. 221 */ 222 public Optional<JsonMap> asMap() { 223 if (! isPresent()) 224 return oe(); 225 226 var keys = configMap.getKeys(name); 227 228 var m = new JsonMap(); 229 for (var k : keys) 230 m.put(k, config.get(name + '/' + k).as(Object.class).orElse(null)); 231 return o(m); 232 } 233 234 /** 235 * Returns <jk>true</jk> if this section exists. 236 * 237 * @return <jk>true</jk> if this section exists. 238 */ 239 public boolean isPresent() { return configMap.hasSection(name); } 240 241 /** 242 * Copies the entries in this section to the specified bean by calling the public setters on that bean. 243 * 244 * @param bean 245 * The bean to set the properties on. 246 * <br>Must not be <jk>null</jk>. 247 * @param ignoreUnknownProperties 248 * If <jk>true</jk>, don't throw an {@link IllegalArgumentException} if this section contains a key that doesn't 249 * correspond to a setter method. 250 * @return This object. 251 * @throws ParseException If parser was not set on this config file or invalid properties were found in the section. 252 */ 253 public Section writeToBean(Object bean, boolean ignoreUnknownProperties) throws ParseException { 254 reqnn("bean", bean); 255 if (! isPresent()) 256 throw iaex("Section '%s' not found in configuration.", name); 257 258 var keys = configMap.getKeys(name); 259 260 var bm = config.marshallingSession.toBeanMap(bean); 261 for (var k : keys) { 262 var bpm = bm.getPropertyMeta(k); 263 if (bpm == null) { 264 if (! ignoreUnknownProperties) 265 throw new ParseException("Unknown property '%s' encountered in configuration section '%s'.", k, name); 266 } else { 267 bm.put(k, config.get(name + '/' + k).as(bpm.getBeanInfo().inner()).orElse(null)); 268 } 269 } 270 271 return this; 272 } 273}