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