001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.config; 014 015import static org.apache.juneau.common.internal.ArgUtils.*; 016import static org.apache.juneau.internal.CollectionUtils.*; 017import java.beans.*; 018import java.lang.reflect.*; 019import java.util.*; 020 021import org.apache.juneau.*; 022import org.apache.juneau.collections.*; 023import org.apache.juneau.config.internal.*; 024import org.apache.juneau.parser.*; 025 026/** 027 * A single section in a config file. 028 */ 029public class Section { 030 031 final Config config; 032 private final ConfigMap configMap; 033 final String name; 034 035 /** 036 * Constructor. 037 * 038 * @param config The config that this entry belongs to. 039 * @param configMap The map that this belongs to. 040 * @param name The section name of this entry. 041 */ 042 protected Section(Config config, ConfigMap configMap, String name) { 043 this.config = config; 044 this.configMap = configMap; 045 this.name = name; 046 } 047 048 /** 049 * Returns <jk>true</jk> if this section exists. 050 * 051 * @return <jk>true</jk> if this section exists. 052 */ 053 public boolean isPresent() { 054 return configMap.hasSection(name); 055 } 056 057 /** 058 * Shortcut for calling <code>asBean(sectionName, c, <jk>false</jk>)</code>. 059 * 060 * @param <T> The bean class to create. 061 * @param c The bean class to create. 062 * @return A new bean instance, or {@link Optional#empty()} if this section does not exist. 063 * @throws ParseException Malformed input encountered. 064 */ 065 public <T> Optional<T> asBean(Class<T> c) throws ParseException { 066 return asBean(c, false); 067 } 068 069 /** 070 * Converts this config file section to the specified bean instance. 071 * 072 * <p> 073 * Key/value pairs in the config file section get copied as bean property values to the specified bean class. 074 * 075 * <h5 class='figure'>Example config file</h5> 076 * <p class='bini'> 077 * <cs>[MyAddress]</cs> 078 * <ck>name</ck> = <cv>John Smith</cv> 079 * <ck>street</ck> = <cv>123 Main Street</cv> 080 * <ck>city</ck> = <cv>Anywhere</cv> 081 * <ck>state</ck> = <cv>NY</cv> 082 * <ck>zip</ck> = <cv>12345</cv> 083 * </p> 084 * 085 * <h5 class='figure'>Example bean</h5> 086 * <p class='bjava'> 087 * <jk>public class</jk> Address { 088 * <jk>public</jk> String <jf>name</jf>, <jf>street</jf>, <jf>city</jf>; 089 * <jk>public</jk> StateEnum <jf>state</jf>; 090 * <jk>public int</jk> <jf>zip</jf>; 091 * } 092 * </p> 093 * 094 * <h5 class='figure'>Example usage</h5> 095 * <p class='bjava'> 096 * Config <jv>config</jv> = Config.<jsm>create</jsm>().name(<js>"MyConfig.cfg"</js>).build(); 097 * Address <jv>address</jv> = <jv>config</jv>.getSection(<js>"MySection"</js>).asBean(Address.<jk>class</jk>).orElse(<jk>null</jk>); 098 * </p> 099 * 100 * @param <T> The bean class to create. 101 * @param c The bean class to create. 102 * @param ignoreUnknownProperties 103 * If <jk>false</jk>, throws a {@link ParseException} if the section contains an entry that isn't a bean property 104 * name. 105 * @return A new bean instance, or <jk>null</jk> if this section doesn't exist. 106 * @throws ParseException Unknown property was encountered in section. 107 */ 108 public <T> Optional<T> asBean(Class<T> c, boolean ignoreUnknownProperties) throws ParseException { 109 assertArgNotNull("c", c); 110 if (! isPresent()) 111 return empty(); 112 113 Set<String> keys = configMap.getKeys(name); 114 115 BeanMap<T> bm = config.beanSession.newBeanMap(c); 116 for (String k : keys) { 117 BeanPropertyMeta bpm = bm.getPropertyMeta(k); 118 if (bpm == null) { 119 if (! ignoreUnknownProperties) 120 throw new ParseException("Unknown property ''{0}'' encountered in configuration section ''{1}''.", k, name); 121 } else { 122 bm.put(k, config.get(name + '/' + k).as(bpm.getClassMeta().getInnerClass()).orElse(null)); 123 } 124 } 125 126 return optional(bm.getBean()); 127 } 128 129 /** 130 * Returns this section as a map. 131 * 132 * @return A new {@link JsonMap}, or {@link Optional#empty()} if this section doesn't exist. 133 */ 134 public Optional<JsonMap> asMap() { 135 if (! isPresent()) 136 return empty(); 137 138 Set<String> keys = configMap.getKeys(name); 139 140 JsonMap m = new JsonMap(); 141 for (String k : keys) 142 m.put(k, config.get(name + '/' + k).as(Object.class).orElse(null)); 143 return optional(m); 144 } 145 146 /** 147 * Wraps this section inside a Java interface so that values in the section can be read and 148 * write using getters and setters. 149 * 150 * <h5 class='figure'>Example config file</h5> 151 * <p class='bini'> 152 * <cs>[MySection]</cs> 153 * <ck>string</ck> = <cv>foo</cv> 154 * <ck>int</ck> = <cv>123</cv> 155 * <ck>enum</ck> = <cv>ONE</cv> 156 * <ck>bean</ck> = <cv>{foo:'bar',baz:123}</cv> 157 * <ck>int3dArray</ck> = <cv>[[[123,null],null],null]</cv> 158 * <ck>bean1d3dListMap</ck> = <cv>{key:[[[[{foo:'bar',baz:123}]]]]}</cv> 159 * </p> 160 * 161 * <h5 class='figure'>Example interface</h5> 162 * <p class='bjava'> 163 * <jk>public interface</jk> MyConfigInterface { 164 * 165 * String getString(); 166 * <jk>void</jk> setString(String <jv>value</jv>); 167 * 168 * <jk>int</jk> getInt(); 169 * <jk>void</jk> setInt(<jk>int</jk> <jv>value</jv>); 170 * 171 * MyEnum getEnum(); 172 * <jk>void</jk> setEnum(MyEnum <jv>value</jv>); 173 * 174 * MyBean getBean(); 175 * <jk>void</jk> setBean(MyBean <jv>value</jv>); 176 * 177 * <jk>int</jk>[][][] getInt3dArray(); 178 * <jk>void</jk> setInt3dArray(<jk>int</jk>[][][] <jv>value</jv>); 179 * 180 * Map<String,List<MyBean[][][]>> getBean1d3dListMap(); 181 * <jk>void</jk> setBean1d3dListMap(Map<String,List<MyBean[][][]>> <jv>value</jv>); 182 * } 183 * </p> 184 * 185 * <h5 class='figure'>Example usage</h5> 186 * <p class='bjava'> 187 * Config <jv>config</jv> = Config.<jsm>create</jsm>().name(<js>"MyConfig.cfg"</js>).build(); 188 * 189 * MyConfigInterface <jv>ci</jv> = <jv>config</jv>.get(<js>"MySection"</js>).asInterface(MyConfigInterface.<jk>class</jk>).orElse(<jk>null</jk>); 190 * 191 * <jk>int</jk> <jv>myInt</jv> = <jv>ci</jv>.getInt(); 192 * 193 * <jv>ci</jv>.setBean(<jk>new</jk> MyBean()); 194 * 195 * <jv>ci</jv>.save(); 196 * </p> 197 * 198 * <h5 class='section'>Notes:</h5><ul> 199 * <li class='note'>Calls to setters when the configuration is read-only will cause {@link UnsupportedOperationException} to be thrown. 200 * </ul> 201 * 202 * @param <T> The proxy interface class. 203 * @param c The proxy interface class. 204 * @return The proxy interface. 205 */ 206 @SuppressWarnings("unchecked") 207 public <T> Optional<T> asInterface(final Class<T> c) { 208 assertArgNotNull("c", c); 209 210 if (! c.isInterface()) 211 throw new IllegalArgumentException("Class '"+c.getName()+"' passed to toInterface() is not an interface."); 212 213 InvocationHandler h = new InvocationHandler() { 214 215 @Override 216 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 217 BeanInfo bi = Introspector.getBeanInfo(c, null); 218 for (PropertyDescriptor pd : bi.getPropertyDescriptors()) { 219 Method rm = pd.getReadMethod(), wm = pd.getWriteMethod(); 220 if (method.equals(rm)) 221 return config.get(name + '/' + pd.getName()).as(rm.getGenericReturnType()).orElse(null); 222 if (method.equals(wm)) 223 return config.set(name + '/' + pd.getName(), args[0]); 224 } 225 throw new UnsupportedOperationException("Unsupported interface method. method='"+method+"'"); 226 } 227 }; 228 229 return optional((T)Proxy.newProxyInstance(c.getClassLoader(), new Class[] { c }, h)); 230 } 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()) throw new IllegalArgumentException("Section '"+name+"' not found in configuration."); 246 247 Set<String> keys = configMap.getKeys(name); 248 249 BeanMap<?> bm = config.beanSession.toBeanMap(bean); 250 for (String k : keys) { 251 BeanPropertyMeta bpm = bm.getPropertyMeta(k); 252 if (bpm == null) { 253 if (! ignoreUnknownProperties) 254 throw new ParseException("Unknown property ''{0}'' encountered in configuration section ''{1}''.", k, name); 255 } else { 256 bm.put(k, config.get(name + '/' + k).as(bpm.getClassMeta().getInnerClass()).orElse(null)); 257 } 258 } 259 260 return this; 261 } 262}