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