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