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&lt;String,List&lt;MyBean[][][]&gt;&gt; getBean1d3dListMap();
181    *       <jk>void</jk> setBean1d3dListMap(Map&lt;String,List&lt;MyBean[][][]&gt;&gt; <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}