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