001/*
002 * jPOS Project [http://jpos.org]
003 * Copyright (C) 2000-2026 jPOS Software SRL
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jpos.q2;
020
021
022import org.jdom2.Content;
023import org.jdom2.Element;
024import org.jdom2.Text;
025import org.jpos.core.*;
026import org.jpos.core.annotation.Config;
027import org.jpos.q2.qbean.QConfig;
028import org.jpos.util.LogSource;
029import org.jpos.util.Logger;
030import org.jpos.util.NameRegistrar;
031import org.jpos.util.Realm;
032
033import javax.management.*;
034import java.lang.reflect.Field;
035import java.lang.reflect.InvocationTargetException;
036import java.lang.reflect.Method;
037import java.net.MalformedURLException;
038import java.util.Collection;
039import java.util.List;
040import java.util.MissingResourceException;
041import java.util.ResourceBundle;
042import java.util.StringTokenizer;
043import java.util.concurrent.ExecutorService;
044import java.util.concurrent.Executors;
045
046/**
047 * Factory responsible for creating, configuring, and deploying QBean instances from XML deploy descriptors.
048 * @author <a href="mailto:taherkordy@dpi2.dpi.net.ir">Alireza Taherkordi</a>
049 * @author <a href="mailto:apr@cs.com.uy">Alejandro P. Revilla</a>
050 * @version $Revision$ $Date$
051 */
052@SuppressWarnings("unchecked")
053public class QFactory {
054    /** ObjectName of the QClassLoader MBean. */
055    ObjectName loaderName;
056    /** The Q2 server instance. */
057    Q2 q2;
058    /** Resource bundle mapping element names to class names. */
059    ResourceBundle classMapping;
060    /** Default configuration factory used when no custom one is specified. */
061    ConfigurationFactory defaultConfigurationFactory = new SimpleConfigurationFactory();
062
063    /**
064     * Constructs a QFactory with the given class loader object name and Q2 instance.
065     *
066     * @param loaderName the ObjectName of the QClassLoader MBean
067     * @param q2         the Q2 server instance
068     */
069    public QFactory (ObjectName loaderName, Q2 q2) {
070        super ();
071        this.loaderName = loaderName;
072        this.q2  = q2;
073        classMapping = ResourceBundle.getBundle(this.getClass().getName());
074    }
075
076    /**
077     * Instantiates the MBean class described by the given XML element.
078     *
079     * @param server the Q2 server instance
080     * @param e      the XML element defining the class to instantiate
081     * @return the newly instantiated object
082     * @throws ReflectionException      if the class cannot be instantiated via reflection
083     * @throws MBeanException           if the MBeanServer reports an error
084     * @throws InstanceNotFoundException if the class loader MBean is not found
085     */
086    @SuppressWarnings("PMD.EmptyCatchBlock")
087    public Object instantiate (Q2 server, Element e)
088        throws ReflectionException,
089               MBeanException,
090               InstanceNotFoundException
091    {
092        String clazz  = getAttributeValue (e, "class");
093        if (clazz == null) {
094            try {
095                clazz = classMapping.getString (e.getName());
096            } catch (MissingResourceException ex) {
097                // no class attribute, no mapping
098                // let MBeanServer do the yelling
099            }
100        }
101        MBeanServer mserver = server.getMBeanServer();
102        if (!q2.isDisableDynamicClassloader())
103            getExtraPath(server.getLoader(), e);
104        return mserver.instantiate (clazz, loaderName);
105    }
106
107    /**
108     * Registers and initializes a QBean MBean in the MBeanServer.
109     *
110     * @param server the Q2 server instance
111     * @param e      the XML element defining the QBean
112     * @param obj    the instantiated QBean object
113     * @return the registered {@link ObjectInstance}
114     * @throws MalformedObjectNameException    if the object name is malformed
115     * @throws InstanceAlreadyExistsException  if a bean with this name is already deployed
116     * @throws InstanceNotFoundException       if the class loader MBean is not found
117     * @throws MBeanException                  if the MBeanServer reports an error
118     * @throws NotCompliantMBeanException      if the object is not a valid MBean
119     * @throws InvalidAttributeValueException  if an attribute value is invalid
120     * @throws ReflectionException             if a reflective operation fails
121     * @throws ConfigurationException          if configuration fails
122     */
123    public ObjectInstance createQBean (Q2 server, Element e, Object obj)
124        throws MalformedObjectNameException,
125               InstanceAlreadyExistsException,
126               InstanceNotFoundException,
127               MBeanException,
128               NotCompliantMBeanException,
129               InvalidAttributeValueException,
130               ReflectionException,
131               ConfigurationException
132    {
133        String name   = getAttributeValue (e, "name");
134        if (name == null)
135            name = e.getName ();
136
137        ObjectName objectName = new ObjectName (Q2.QBEAN_NAME + name);
138        MBeanServer mserver = server.getMBeanServer();
139        if(mserver.isRegistered(objectName)) {
140            throw new InstanceAlreadyExistsException (name+" has already been deployed in another file.");
141        }
142        ObjectInstance instance = mserver.registerMBean (
143            obj, objectName
144        );
145        try {
146            setAttribute (mserver, objectName, "Name", name);
147            String logger = getAttributeValue (e, "logger");
148            if (logger != null)
149                setAttribute (mserver, objectName, "Logger", logger);
150            String realm = getAttributeValue (e, "realm");
151            if (realm != null)
152                setAttribute (mserver, objectName, "Realm", realm);
153            setAttribute (mserver, objectName, "Server", server);
154            setAttribute (mserver, objectName, "Persist", e);
155            configureQBean(mserver,objectName,e);
156            setConfiguration (obj, e);  // handle legacy (QSP v1) Configurables
157
158            if (obj instanceof QBean)
159                mserver.invoke (objectName, "init",  null, null);
160        }
161        catch (Throwable t) {
162            mserver.unregisterMBean(objectName);
163            t.fillInStackTrace();
164            throw t;
165        }
166
167        return instance;
168    }
169    /**
170     * Returns the Q2 server instance associated with this factory.
171     *
172     * @return the Q2 server instance
173     */
174    public Q2 getQ2() {
175        return q2;
176    }
177
178    private void getExtraPath (QClassLoader loader, Element e) {
179        Element classpathElement = e.getChild ("classpath");
180        if (classpathElement != null) {
181            try {
182                loader = loader.scan (true);    // force a new classloader
183            } catch (Throwable t) {
184                getQ2().getLog().error(t);
185            }
186            for (Element u : classpathElement.getChildren("url")) {
187                try {
188                    loader.addURL(u.getTextTrim());
189                } catch (MalformedURLException ex) {
190                    q2.getLog().warn(u.getTextTrim(), ex);
191                }
192            }
193        }
194    }
195
196    /**
197     * Sets an attribute on the given MBean, silently ignoring unknown attributes.
198     *
199     * @param server     the MBeanServer
200     * @param objectName the ObjectName of the MBean
201     * @param attribute  the attribute name
202     * @param value      the attribute value
203     * @throws InstanceNotFoundException     if the MBean is not found
204     * @throws MBeanException                if the MBeanServer reports an error
205     * @throws InvalidAttributeValueException if the value is invalid
206     * @throws ReflectionException           if a reflective operation fails
207     */
208    @SuppressWarnings("PMD.EmptyCatchBlock")
209    public void setAttribute
210        (MBeanServer server, ObjectName objectName,
211         String attribute, Object value)
212        throws InstanceNotFoundException,
213               MBeanException,
214               InvalidAttributeValueException,
215               ReflectionException
216    {
217        try {
218            server.setAttribute (
219                objectName, new Attribute (attribute, value)
220            );
221        } catch (AttributeNotFoundException ex) {
222            // okay to fail
223        } catch (InvalidAttributeValueException ex) {
224            // okay to fail (produced by some application servers instead of AttributeNotFoundException)
225        }
226    }
227
228    /**
229     * Invokes the {@code start} method on the given QBean MBean.
230     *
231     * @param server     the Q2 server instance
232     * @param objectName the ObjectName of the QBean MBean
233     * @throws InstanceNotFoundException if the MBean is not found
234     * @throws MBeanException            if the MBeanServer reports an error
235     * @throws ReflectionException       if a reflective operation fails
236     */
237    public void startQBean (Q2 server, ObjectName objectName)
238        throws InstanceNotFoundException,
239               MBeanException,
240               ReflectionException
241    {
242        MBeanServer mserver = server.getMBeanServer();
243        mserver.invoke (objectName, "start",  null, null);
244    }
245
246    /**
247     * Stops, destroys, and unregisters the given QBean MBean.
248     *
249     * @param server     the Q2 server instance
250     * @param objectName the ObjectName of the QBean MBean
251     * @param obj        the underlying QBean object
252     * @throws InstanceNotFoundException if the MBean is not found
253     * @throws MBeanException            if the MBeanServer reports an error
254     * @throws ReflectionException       if a reflective operation fails
255     */
256    public void destroyQBean (Q2 server, ObjectName objectName, Object obj)
257        throws InstanceNotFoundException,
258               MBeanException,
259               ReflectionException
260    {
261        MBeanServer mserver = server.getMBeanServer();
262        if (obj instanceof QBean) {
263            mserver.invoke (objectName, "stop",  null, null);
264            mserver.invoke (objectName, "destroy",  null, null);
265        }
266        if (objectName != null)
267            mserver.unregisterMBean (objectName);
268    }
269
270    /**
271     * Configures a QBean by applying attributes from the given XML element.
272     *
273     * @param server     the MBeanServer
274     * @param objectName the ObjectName of the QBean
275     * @param e          the XML element containing {@code attr} child elements
276     * @throws ConfigurationException if attribute setting fails
277     */
278    public void configureQBean(MBeanServer server, ObjectName objectName, Element e)
279        throws ConfigurationException
280    {
281        try {
282            AttributeList attributeList = getAttributeList(e);
283            for (Object anAttributeList : attributeList)
284                server.setAttribute(objectName, (Attribute) anAttributeList);
285        } catch (Exception e1) {
286            throw new ConfigurationException(e1);
287        }
288
289    }
290    /**
291     * Builds a JMX {@link AttributeList} from {@code attr} child elements of the given element.
292     *
293     * @param e the XML element containing {@code attr} child elements
294     * @return list of JMX Attribute objects
295     * @throws ConfigurationException if attribute conversion fails
296     */
297    public AttributeList getAttributeList(Element e)
298        throws ConfigurationException
299    {
300        AttributeList attributeList = new AttributeList();
301        List childs = e.getChildren("attr");
302        for (Object child : childs) {
303            Element childElement = (Element) child;
304            String name = childElement.getAttributeValue("name");
305            name = getAttributeName(name);
306            Attribute attr = new Attribute(name, getObject(childElement));
307            attributeList.add(attr);
308        }
309        return attributeList;
310    }
311
312    /**
313     * Creates an object from a definition element.
314     * The element may have an attribute called type indicating the type of the object
315     * to create, if this attribute is not present java.lang.String is assumed.
316     * int, long and boolean are converted to their wrappers.
317     * @return The created object.
318     * @param childElement Dom Element with the definition of the object.
319     * @throws ConfigurationException If an exception is found trying to create the object.
320     */
321    @SuppressWarnings("unchecked")
322    protected Object getObject(Element childElement)
323        throws ConfigurationException
324    {
325        String type = childElement.getAttributeValue("type","java.lang.String");
326        if ("int".equals (type))
327            type = "java.lang.Integer";
328        else if ("long".equals (type))
329            type = "java.lang.Long";
330        else if ("boolean".equals (type))
331            type = "java.lang.Boolean";
332
333        String value = childElement.getText();
334        value = Environment.getEnvironment().getProperty(value, value);
335        try {
336            Class<?> attributeType = Class.forName(type);
337            if(Collection.class.isAssignableFrom(attributeType))
338                return getCollection(attributeType, childElement);
339            else{
340                Class[] parameterTypes = {"".getClass()};
341                Object[] parameterValues = {value};
342                return attributeType.getConstructor(parameterTypes).newInstance(parameterValues);
343            }
344        } catch (Exception e1) {
345            throw new ConfigurationException(e1);
346        }
347
348    }
349
350
351    /** Creates a collection from a definition element with the format.
352     * <pre>{@code
353     *    <attr type="...">
354     *        <item type="...">...</item>
355     *        ...
356     *    </attr>
357     * }</pre>
358     * @param type class type
359     * @param e the Element
360     * @throws ConfigurationException if configuration is invalid
361     * @return the object collection
362     */
363    @SuppressWarnings("unchecked")
364    protected Collection getCollection(Class type, Element e)
365        throws ConfigurationException
366    {
367        try{
368            Collection<Object> col = (Collection<Object>) type.newInstance();
369            for (Element o : e.getChildren("item")) {
370                col.add(getObject(o));
371            }
372            return col;
373        }catch(Exception e1){
374            throw new ConfigurationException(e1);
375        }
376    }
377    /**
378     * Capitalizes the first letter of an attribute name to produce a setter/getter name.
379     *
380     * @param name the raw attribute name
381     * @return the capitalized attribute name
382     */
383    public String getAttributeName(String name)
384    {
385        if (name == null)
386            throw new NullPointerException("attribute name can not be null");
387        StringBuilder tmp = new StringBuilder(name);
388        if (tmp.length() > 0)
389            tmp.setCharAt(0,name.toUpperCase().charAt(0)) ;
390        return tmp.toString();
391    }
392
393    /**
394     * Instantiates the class with the given name via the Q2 MBeanServer.
395     *
396     * @param <T>   the expected return type
397     * @param clazz the fully-qualified class name to instantiate
398     * @return the newly instantiated object
399     * @throws ConfigurationException if instantiation fails
400     */
401    public <T> T newInstance (String clazz)
402        throws ConfigurationException
403    {
404        try {
405            MBeanServer mserver = q2.getMBeanServer();
406            return (T)mserver.instantiate (clazz, loaderName);
407        } catch (Exception e) {
408            throw new ConfigurationException (clazz, e);
409        }
410    }
411
412    /**
413     * Instantiates the given class via the Q2 MBeanServer.
414     *
415     * @param <T>   the expected return type
416     * @param clazz the Class to instantiate
417     * @return the newly instantiated object
418     * @throws ConfigurationException if instantiation fails
419     */
420    public <T> T newInstance (Class<T> clazz)
421            throws ConfigurationException
422    {
423        return newInstance(clazz.getName());
424    }
425
426    /**
427     * Creates a new instance from the values in {@code Element e}.<br/>
428     * <p>
429     * The method honors the {@code enabled} attribute in the given {@code Element e},
430     * returning null immediately if {@code enabled} is computed to a true-equivalent.</p>
431     * <p>
432     * It also calls {@link #setLogger} to set logger and realm, and {@link #setConfiguration(Object, Element)}
433     * to trigger the standard [auto]configuration sequence from properties and XML.</p>
434     *
435     * @param <T> expected concrete type returned to the caller
436     * @param e The XML config
437     * @return the new instance, or null if not enabled
438     * @throws ConfigurationException if the instance can't be created (e.g. class not found)
439     *                                or the configuration process itself threw the exception.
440     */
441    public <T> T newInstance(Element e) throws ConfigurationException {
442        if (!QFactory.isEnabled(e))
443            return null;
444        T obj = newInstance(QFactory.getAttributeValue (e, "class"));
445        setLogger       (obj, e);
446        setConfiguration(obj, e);
447        return obj;
448    }
449
450    /**
451     * Returns a {@link Configuration} built from the given XML element, with optional merge.
452     *
453     * @param e the XML element to build the configuration from
454     * @return the resulting Configuration
455     * @throws ConfigurationException if configuration building or merging fails
456     */
457    public Configuration getConfiguration (Element e)
458        throws ConfigurationException
459    {
460        String configurationFactoryClazz = getAttributeValue(e, "configuration-factory");
461        ConfigurationFactory cf = configurationFactoryClazz != null ?
462            (ConfigurationFactory) newInstance(configurationFactoryClazz) : defaultConfigurationFactory;
463
464        Configuration cfg = cf.getConfiguration(e);
465        String merge = getAttributeValue(e, "merge-configuration");
466        if (merge != null) {
467            StringTokenizer st = new StringTokenizer(merge, ", ");
468            while (st.hasMoreElements()) {
469                try {
470                    Configuration c = QConfig.getConfiguration(st.nextToken());
471                    for (String k : c.keySet()) {
472                        if (cfg.get(k, null) == null) {
473                            String[] v = c.getAll(k);
474                            switch (v.length) {
475                                case 0:
476                                    break;
477                                case 1:
478                                    cfg.put(k, v[0]);
479                                    break;
480                                default:
481                                    cfg.put(k, v);
482                            }
483                        }
484                    }
485                } catch (NameRegistrar.NotFoundException ex) {
486                    throw new ConfigurationException (ex.getMessage());
487                }
488            }
489        }
490        return cfg;
491    }
492
493    /**
494     * Sets the logger and realm on the object if it implements {@link LogSource}.
495     *
496     * @param obj the object to configure
497     * @param e   the XML element containing logger/realm attributes
498     */
499    public void setLogger (Object obj, Element e) {
500        setLogger(obj, e, null);
501    }
502
503    /**
504     * Sets the logger and realm on the object if it implements {@link LogSource}, with a fallback realm.
505     *
506     * @param obj           the object to configure
507     * @param e             the XML element containing logger/realm attributes
508     * @param fallbackRealm realm to use when none is specified in the element
509     */
510    public void setLogger (Object obj, Element e, String fallbackRealm) {
511        if (obj instanceof LogSource) {
512            String loggerName = getAttributeValue (e, "logger");
513            if (loggerName != null) {
514                String realm = getAttributeValue (e, "realm");
515                if (realm == null)
516                    realm = fallbackRealm != null ? fallbackRealm : defaultRealm(e.getName());
517                Logger logger = Logger.getLogger (loggerName);
518                ((LogSource)obj).setLogger (logger, realm);
519            }
520        }
521    }
522
523    /**
524     * Returns the default realm name for the given XML element name.
525     *
526     * @param elementName the XML element name (e.g. "channel", "server")
527     * @return the corresponding default realm string
528     */
529    protected String defaultRealm(String elementName) {
530        return switch (elementName) {
531            case "channel" -> Realm.COMM_CHANNEL;
532            case "server" -> Realm.COMM_SERVER;
533            case "mux" -> Realm.COMM_MUX;
534            case "client" -> Realm.COMM_CLIENT;
535            default -> elementName;
536        };
537    }
538
539    /**
540     * Returns the value of the named attribute from the element, with environment variable substitution.
541     *
542     * @param e    the XML element
543     * @param name the attribute name
544     * @return the resolved attribute value, or {@code null} if not present
545     */
546    public static String getAttributeValue (Element e, String name) {
547        String s = e.getAttributeValue(name);
548        return Environment.getEnvironment().getProperty(s, s);
549    }
550
551    /**
552     * Applies configuration from the XML element to the given object.
553     *
554     * @param obj the object to configure
555     * @param e   the XML element containing configuration attributes
556     * @throws ConfigurationException if configuration fails
557     */
558    public void setConfiguration (Object obj, Element e)
559        throws ConfigurationException
560    {
561        try {
562            Configuration cfg = getConfiguration (e);
563            autoconfigure(obj, cfg);
564
565            if (obj instanceof Configurable)
566                ((Configurable)obj).setConfiguration (cfg);
567            if (obj instanceof XmlConfigurable)
568                ((XmlConfigurable)obj).setConfiguration(e);
569        } catch (ConfigurationException | IllegalAccessException ex) {
570            throw new ConfigurationException (ex);
571        }
572    }
573   /**
574    * Try to invoke a method (usually a setter) on the given object
575    * silently ignoring if method does not exist
576    * @param obj the object
577    * @param m method to invoke
578    * @param p parameter
579    * @throws ConfigurationException if method happens to throw an exception
580    */
581    public static void invoke (Object obj, String m, Object p)
582        throws ConfigurationException
583    {
584        invoke (obj, m, p, p != null ? p.getClass() : null);
585    }
586
587   /**
588    * Try to invoke a method (usually a setter) on the given object
589    * silently ignoring if method does not exist
590    * @param obj the object
591    * @param m method to invoke
592    * @param p parameter
593    * @param pc parameter class
594    * @throws ConfigurationException if method happens to throw an exception
595    */
596   @SuppressWarnings("PMD.EmptyCatchBlock")
597    public static void invoke (Object obj, String m, Object p, Class pc)
598        throws ConfigurationException
599    {
600        try {
601            if (p!=null) {
602                Class[] paramTemplate = { pc };
603                Method method = obj.getClass().getMethod(m, paramTemplate);
604                Object[] param = new Object[1];
605                param[0] = p;
606                method.invoke (obj, param);
607            } else {
608                Method method = obj.getClass().getMethod(m);
609                method.invoke (obj);
610           }
611        } catch (NoSuchMethodException ignored) {
612        } catch (NullPointerException ignored) {
613        } catch (IllegalAccessException ignored) {
614        } catch (InvocationTargetException e) {
615            throw new ConfigurationException (
616                obj.getClass().getName() + "." + m + "(" + p +")" ,
617                    e.getTargetException()
618            );
619        }
620    }
621    /**
622     * Returns {@code true} if the element's {@code enabled} attribute evaluates to true.
623     *
624     * @param e the XML element
625     * @return {@code true} if enabled
626     */
627    public static boolean isEnabled(Element e) {
628        return isTrue(getEnabledAttribute(e));
629    }
630
631    /**
632     * Returns {@code true} if the element's {@code eager-start} attribute evaluates to true.
633     *
634     * @param e the XML element
635     * @return {@code true} if eager start is requested
636     */
637    public static boolean isEagerStart(Element e) {
638        return isTrue(getEagerStartAttribute(e));
639    }
640    private static boolean isTrue(String attribute) {
641        return "true".equalsIgnoreCase(attribute) ||
642          "yes".equalsIgnoreCase(attribute) ||
643          attribute.contains(Environment.getEnvironment().getName());
644    }
645
646    /**
647     * Returns the value of the {@code enabled} attribute, defaulting to {@code "true"}.
648     *
649     * @param e the XML element
650     * @return the enabled attribute value
651     */
652    public static String getEnabledAttribute (Element e) {
653        return getAttribute(e, "enabled", "true");
654    }
655
656    /**
657     * Returns the value of the {@code eager-start} attribute, defaulting to {@code "false"}.
658     *
659     * @param e the XML element
660     * @return the eager-start attribute value
661     */
662    public static String getEagerStartAttribute (Element e) {
663        return getAttribute(e, "eager-start", "false");
664    }
665    private static String getAttribute (Element e, String attr, String def) {
666        return Environment.get(e.getAttributeValue(attr, def));
667    }
668
669    /**
670     * Automatically configures fields annotated with {@link Config} on the given object.
671     *
672     * @param obj the object to autoconfigure
673     * @param cfg the Configuration to read values from
674     * @throws IllegalAccessException if field access fails
675     */
676    @SuppressWarnings("rawtypes")
677    public static void autoconfigure (Object obj, Configuration cfg) throws IllegalAccessException {
678        Class cc = obj.getClass();
679        do {
680            Field[] fields = cc.getDeclaredFields();
681            for (Field field : fields) {
682                if (field.isAnnotationPresent(Config.class)) {
683                    Config config = field.getAnnotation(Config.class);
684                    String v = cfg.get(config.value(), null);
685                    if (v != null) {
686                        if (!field.isAccessible())
687                            field.setAccessible(true);
688                        Class<?> c = field.getType();
689                        if (c.isAssignableFrom(String.class))
690                            field.set(obj, v);
691                        else if (c.isAssignableFrom(int.class) || c.isAssignableFrom(Integer.class))
692                            field.set(obj, cfg.getInt(config.value()));
693                        else if (c.isAssignableFrom(long.class) || c.isAssignableFrom(Long.class))
694                            field.set(obj, cfg.getLong(config.value()));
695                        else if (c.isAssignableFrom(double.class) || c.isAssignableFrom(Double.class))
696                            field.set(obj, cfg.getDouble(config.value()));
697                        else if (c.isAssignableFrom(boolean.class) || c.isAssignableFrom(Boolean.class))
698                            field.set(obj, cfg.getBoolean(config.value()));
699                        else if (c.isEnum())
700                            field.set(obj, Enum.valueOf((Class<Enum>) c, v));
701                        else if (c.isArray()) {
702                            Class<?> ct = c.getComponentType();
703                            if (ct.isAssignableFrom(String.class))
704                                field.set(obj, cfg.getAll(config.value()));
705                            else if (ct.isAssignableFrom(int.class) || ct.isAssignableFrom(Integer.class))
706                                field.set(obj, cfg.getInts(config.value()));
707                            else if (ct.isAssignableFrom(long.class) || ct.isAssignableFrom(Long.class))
708                                field.set(obj, cfg.getLongs(config.value()));
709                            else if (ct.isAssignableFrom(double.class) || ct.isAssignableFrom(Double.class))
710                                field.set(obj, cfg.getDoubles(config.value()));
711                        }
712                    }
713                }
714            }
715            cc = cc.getSuperclass();
716        } while (!cc.equals(Object.class));
717    }
718
719    /**
720     * Decorates an {@link Element} by replacing its attributes, and content {@link Environment} properties references.
721     * @param e The element being decorated.
722     * @return The modified element, it is modified in place, but it is returned to ease method chaining or call composition.
723     */
724    public static Element expandEnvProperties(Element e) {
725       expandEnvProperties(e, Environment.getEnvironment());
726       return e;
727    }
728
729    /**
730     * Creates an {@link ExecutorService} using either virtual or platform threads.
731     *
732     * @param virtual {@code true} to use virtual threads, {@code false} for platform threads
733     * @return a new ExecutorService
734     */
735    public static ExecutorService executorService(boolean virtual) {
736        return virtual ?
737            Executors.newVirtualThreadPerTaskExecutor() :
738            Executors.newThreadPerTaskExecutor(
739              Thread.ofPlatform().inheritInheritableThreadLocals(true)
740                .factory()
741            );
742    }
743
744    /**
745     * Recursively replaces {@link Environment} properties in an element's attributes, its content and its children.
746     * Properties are replaced in place.
747     * @param e The element in which properties are being replaced.
748     * @param env The {@link Environment}'s singleton instance.
749     */
750    private static void expandEnvProperties(Element e, Environment env) {
751        if (Boolean.parseBoolean(e.getAttributeValue("verbatim"))) return;
752        for (org.jdom2.Attribute attr : e.getAttributes()) {
753            String value = attr.getValue();
754            attr.setValue(env.getProperty(value, value));
755        }
756        for (Content child : e.getContent()) {
757            if (child instanceof Element) {
758                expandEnvProperties((Element) child, env);
759            } else if (child instanceof Text text) {
760                String textValue = text.getText();
761                text.setText(env.getProperty(textValue, textValue));
762            }
763        }
764    }
765
766}