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