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;
031
032import javax.management.*;
033import java.lang.reflect.Field;
034import java.lang.reflect.InvocationTargetException;
035import java.lang.reflect.Method;
036import java.net.MalformedURLException;
037import java.util.Collection;
038import java.util.List;
039import java.util.MissingResourceException;
040import java.util.ResourceBundle;
041import java.util.StringTokenizer;
042import java.util.concurrent.ExecutorService;
043import java.util.concurrent.Executors;
044
045/**
046 * @author <a href="mailto:taherkordy@dpi2.dpi.net.ir">Alireza Taherkordi</a>
047 * @author <a href="mailto:apr@cs.com.uy">Alejandro P. Revilla</a>
048 * @version $Revision$ $Date$
049 */
050@SuppressWarnings("unchecked")
051public class QFactory {
052    ObjectName loaderName;
053    Q2 q2;
054    ResourceBundle classMapping;
055    ConfigurationFactory defaultConfigurationFactory = new SimpleConfigurationFactory();
056
057    public QFactory (ObjectName loaderName, Q2 q2) {
058        super ();
059        this.loaderName = loaderName;
060        this.q2  = q2;
061        classMapping = ResourceBundle.getBundle(this.getClass().getName());
062    }
063
064    @SuppressWarnings("PMD.EmptyCatchBlock")
065    public Object instantiate (Q2 server, Element e)
066        throws ReflectionException,
067               MBeanException,
068               InstanceNotFoundException
069    {
070        String clazz  = getAttributeValue (e, "class");
071        if (clazz == null) {
072            try {
073                clazz = classMapping.getString (e.getName());
074            } catch (MissingResourceException ex) {
075                // no class attribute, no mapping
076                // let MBeanServer do the yelling
077            }
078        }
079        MBeanServer mserver = server.getMBeanServer();
080        if (!q2.isDisableDynamicClassloader())
081            getExtraPath(server.getLoader(), e);
082        return mserver.instantiate (clazz, loaderName);
083    }
084
085    public ObjectInstance createQBean (Q2 server, Element e, Object obj)
086        throws MalformedObjectNameException,
087               InstanceAlreadyExistsException,
088               InstanceNotFoundException,
089               MBeanException,
090               NotCompliantMBeanException,
091               InvalidAttributeValueException,
092               ReflectionException,
093               ConfigurationException
094    {
095        String name   = getAttributeValue (e, "name");
096        if (name == null)
097            name = e.getName ();
098
099        ObjectName objectName = new ObjectName (Q2.QBEAN_NAME + name);
100        MBeanServer mserver = server.getMBeanServer();
101        if(mserver.isRegistered(objectName)) {
102            throw new InstanceAlreadyExistsException (name+" has already been deployed in another file.");
103        }
104        ObjectInstance instance = mserver.registerMBean (
105            obj, objectName
106        );
107        try {
108            setAttribute (mserver, objectName, "Name", name);
109            String logger = getAttributeValue (e, "logger");
110            if (logger != null)
111                setAttribute (mserver, objectName, "Logger", logger);
112            String realm = getAttributeValue (e, "realm");
113            if (realm != null)
114                setAttribute (mserver, objectName, "Realm", realm);
115            setAttribute (mserver, objectName, "Server", server);
116            setAttribute (mserver, objectName, "Persist", e);
117            configureQBean(mserver,objectName,e);
118            setConfiguration (obj, e);  // handle legacy (QSP v1) Configurables
119
120            if (obj instanceof QBean)
121                mserver.invoke (objectName, "init",  null, null);
122        }
123        catch (Throwable t) {
124            mserver.unregisterMBean(objectName);
125            t.fillInStackTrace();
126            throw t;
127        }
128
129        return instance;
130    }
131    public Q2 getQ2() {
132        return q2;
133    }
134
135    private void getExtraPath (QClassLoader loader, Element e) {
136        Element classpathElement = e.getChild ("classpath");
137        if (classpathElement != null) {
138            try {
139                loader = loader.scan (true);    // force a new classloader
140            } catch (Throwable t) {
141                getQ2().getLog().error(t);
142            }
143            for (Element u : classpathElement.getChildren("url")) {
144                try {
145                    loader.addURL(u.getTextTrim());
146                } catch (MalformedURLException ex) {
147                    q2.getLog().warn(u.getTextTrim(), ex);
148                }
149            }
150        }
151    }
152
153    @SuppressWarnings("PMD.EmptyCatchBlock")
154    public void setAttribute
155        (MBeanServer server, ObjectName objectName,
156         String attribute, Object value)
157        throws InstanceNotFoundException,
158               MBeanException,
159               InvalidAttributeValueException,
160               ReflectionException
161    {
162        try {
163            server.setAttribute (
164                objectName, new Attribute (attribute, value)
165            );
166        } catch (AttributeNotFoundException ex) {
167            // okay to fail
168        } catch (InvalidAttributeValueException ex) {
169            // okay to fail (produced by some application servers instead of AttributeNotFoundException)
170        }
171    }
172
173    public void startQBean (Q2 server, ObjectName objectName)
174        throws InstanceNotFoundException,
175               MBeanException,
176               ReflectionException
177    {
178        MBeanServer mserver = server.getMBeanServer();
179        mserver.invoke (objectName, "start",  null, null);
180    }
181
182    public void destroyQBean (Q2 server, ObjectName objectName, Object obj)
183        throws InstanceNotFoundException,
184               MBeanException,
185               ReflectionException
186    {
187        MBeanServer mserver = server.getMBeanServer();
188        if (obj instanceof QBean) {
189            mserver.invoke (objectName, "stop",  null, null);
190            mserver.invoke (objectName, "destroy",  null, null);
191        }
192        if (objectName != null)
193            mserver.unregisterMBean (objectName);
194    }
195
196    public void configureQBean(MBeanServer server, ObjectName objectName, Element e)
197        throws ConfigurationException
198    {
199        try {
200            AttributeList attributeList = getAttributeList(e);
201            for (Object anAttributeList : attributeList)
202                server.setAttribute(objectName, (Attribute) anAttributeList);
203        } catch (Exception e1) {
204            throw new ConfigurationException(e1);
205        }
206
207    }
208    public AttributeList getAttributeList(Element e)
209        throws ConfigurationException
210    {
211        AttributeList attributeList = new AttributeList();
212        List childs = e.getChildren("attr");
213        for (Object child : childs) {
214            Element childElement = (Element) child;
215            String name = childElement.getAttributeValue("name");
216            name = getAttributeName(name);
217            Attribute attr = new Attribute(name, getObject(childElement));
218            attributeList.add(attr);
219        }
220        return attributeList;
221    }
222
223    /**
224     * Creates an object from a definition element.
225     * The element may have an attribute called type indicating the type of the object
226     * to create, if this attribute is not present java.lang.String is assumed.
227     * int, long and boolean are converted to their wrappers.
228     * @return The created object.
229     * @param childElement Dom Element with the definition of the object.
230     * @throws ConfigurationException If an exception is found trying to create the object.
231     */
232    @SuppressWarnings("unchecked")
233    protected Object getObject(Element childElement)
234        throws ConfigurationException
235    {
236        String type = childElement.getAttributeValue("type","java.lang.String");
237        if ("int".equals (type))
238            type = "java.lang.Integer";
239        else if ("long".equals (type))
240            type = "java.lang.Long";
241        else if ("boolean".equals (type))
242            type = "java.lang.Boolean";
243
244        String value = childElement.getText();
245        value = Environment.getEnvironment().getProperty(value, value);
246        try {
247            Class<?> attributeType = Class.forName(type);
248            if(Collection.class.isAssignableFrom(attributeType))
249                return getCollection(attributeType, childElement);
250            else{
251                Class[] parameterTypes = {"".getClass()};
252                Object[] parameterValues = {value};
253                return attributeType.getConstructor(parameterTypes).newInstance(parameterValues);
254            }
255        } catch (Exception e1) {
256            throw new ConfigurationException(e1);
257        }
258
259    }
260
261
262    /** Creats a collection from a definition element with the format.
263     * <PRE>
264     *    <{attr|item} type="...">
265     *        <item [type="..."]>...</item>
266     *        ...
267     *    </attr>
268     * </PRE>
269     * @param type class type
270     * @param e the Element
271     * @throws ConfigurationException
272     * @return the object collection
273     */
274    @SuppressWarnings("unchecked")
275    protected Collection getCollection(Class type, Element e)
276        throws ConfigurationException
277    {
278        try{
279            Collection<Object> col = (Collection<Object>) type.newInstance();
280            for (Element o : e.getChildren("item")) {
281                col.add(getObject(o));
282            }
283            return col;
284        }catch(Exception e1){
285            throw new ConfigurationException(e1);
286        }
287    }
288    /**
289     * sets the first character of the string to the upper case
290     * @param name attribute name
291     * @return attribute name
292     */
293    public String getAttributeName(String name)
294    {
295        if (name == null)
296            throw new NullPointerException("attribute name can not be null");
297        StringBuilder tmp = new StringBuilder(name);
298        if (tmp.length() > 0)
299            tmp.setCharAt(0,name.toUpperCase().charAt(0)) ;
300        return tmp.toString();
301    }
302
303    public <T> T newInstance (String clazz)
304        throws ConfigurationException
305    {
306        try {
307            MBeanServer mserver = q2.getMBeanServer();
308            return (T)mserver.instantiate (clazz, loaderName);
309        } catch (Exception e) {
310            throw new ConfigurationException (clazz, e);
311        }
312    }
313
314    public <T> T newInstance (Class<T> clazz)
315            throws ConfigurationException
316    {
317        return newInstance(clazz.getName());
318    }
319
320    /**
321     * Creates a new instance from the values in {@code Element e}.<br/>
322     * <p>
323     * The method honors the {@code enabled} attribute in the given {@code Element e},
324     * returning null immediately if {@code enabled} is computed to a true-equivalent.</p>
325     * <p>
326     * It also calls {@link #setLogger} to set logger and realm, and {@link #setConfiguration(Object, Element)}
327     * to trigger the standard [auto]configuration sequence from properties and XML.</p>
328     *
329     * @param e The XML config
330     * @return the new instance, or null if not enabled
331     * @throws ConfigurationException if the instance can't be created (e.g. class not found)
332     *                                or the configuration process itself threw the exception.
333     */
334    public <T> T newInstance(Element e) throws ConfigurationException {
335        if (!QFactory.isEnabled(e))
336            return null;
337        T obj = newInstance(QFactory.getAttributeValue (e, "class"));
338        setLogger       (obj, e);
339        setConfiguration(obj, e);
340        return obj;
341    }
342
343    public Configuration getConfiguration (Element e)
344        throws ConfigurationException
345    {
346        String configurationFactoryClazz = getAttributeValue(e, "configuration-factory");
347        ConfigurationFactory cf = configurationFactoryClazz != null ?
348            (ConfigurationFactory) newInstance(configurationFactoryClazz) : defaultConfigurationFactory;
349
350        Configuration cfg = cf.getConfiguration(e);
351        String merge = getAttributeValue(e, "merge-configuration");
352        if (merge != null) {
353            StringTokenizer st = new StringTokenizer(merge, ", ");
354            while (st.hasMoreElements()) {
355                try {
356                    Configuration c = QConfig.getConfiguration(st.nextToken());
357                    for (String k : c.keySet()) {
358                        if (cfg.get(k, null) == null) {
359                            String[] v = c.getAll(k);
360                            switch (v.length) {
361                                case 0:
362                                    break;
363                                case 1:
364                                    cfg.put(k, v[0]);
365                                    break;
366                                default:
367                                    cfg.put(k, v);
368                            }
369                        }
370                    }
371                } catch (NameRegistrar.NotFoundException ex) {
372                    throw new ConfigurationException (ex.getMessage());
373                }
374            }
375        }
376        return cfg;
377    }
378
379    public void setLogger (Object obj, Element e) {
380        if (obj instanceof LogSource) {
381            String loggerName = getAttributeValue (e, "logger");
382            if (loggerName != null) {
383                String realm = getAttributeValue (e, "realm");
384                if (realm == null)
385                    realm = e.getName();
386                Logger logger = Logger.getLogger (loggerName);
387                ((LogSource)obj).setLogger (logger, realm);
388            }
389        }
390    }
391
392    public static String getAttributeValue (Element e, String name) {
393        String s = e.getAttributeValue(name);
394        return Environment.getEnvironment().getProperty(s, s);
395    }
396    public void setConfiguration (Object obj, Element e)
397        throws ConfigurationException
398    {
399        try {
400            Configuration cfg = getConfiguration (e);
401            autoconfigure(obj, cfg);
402
403            if (obj instanceof Configurable)
404                ((Configurable)obj).setConfiguration (cfg);
405            if (obj instanceof XmlConfigurable)
406                ((XmlConfigurable)obj).setConfiguration(e);
407        } catch (ConfigurationException | IllegalAccessException ex) {
408            throw new ConfigurationException (ex);
409        }
410    }
411   /**
412    * Try to invoke a method (usually a setter) on the given object
413    * silently ignoring if method does not exist
414    * @param obj the object
415    * @param m method to invoke
416    * @param p parameter
417    * @throws ConfigurationException if method happens to throw an exception
418    */
419    public static void invoke (Object obj, String m, Object p)
420        throws ConfigurationException
421    {
422        invoke (obj, m, p, p != null ? p.getClass() : null);
423    }
424
425   /**
426    * Try to invoke a method (usually a setter) on the given object
427    * silently ignoring if method does not exist
428    * @param obj the object
429    * @param m method to invoke
430    * @param p parameter
431    * @param pc parameter class
432    * @throws ConfigurationException if method happens to throw an exception
433    */
434   @SuppressWarnings("PMD.EmptyCatchBlock")
435    public static void invoke (Object obj, String m, Object p, Class pc)
436        throws ConfigurationException
437    {
438        try {
439            if (p!=null) {
440                Class[] paramTemplate = { pc };
441                Method method = obj.getClass().getMethod(m, paramTemplate);
442                Object[] param = new Object[1];
443                param[0] = p;
444                method.invoke (obj, param);
445            } else {
446                Method method = obj.getClass().getMethod(m);
447                method.invoke (obj);
448           }
449        } catch (NoSuchMethodException ignored) {
450        } catch (NullPointerException ignored) {
451        } catch (IllegalAccessException ignored) {
452        } catch (InvocationTargetException e) {
453            throw new ConfigurationException (
454                obj.getClass().getName() + "." + m + "(" + p +")" ,
455                    e.getTargetException()
456            );
457        }
458    }
459    public static boolean isEnabled(Element e) {
460        return isTrue(getEnabledAttribute(e));
461    }
462    public static boolean isEagerStart(Element e) {
463        return isTrue(getEagerStartAttribute(e));
464    }
465    private static boolean isTrue(String attribute) {
466        return "true".equalsIgnoreCase(attribute) ||
467          "yes".equalsIgnoreCase(attribute) ||
468          attribute.contains(Environment.getEnvironment().getName());
469    }
470
471    public static String getEnabledAttribute (Element e) {
472        return getAttribute(e, "enabled", "true");
473    }
474    public static String getEagerStartAttribute (Element e) {
475        return getAttribute(e, "eager-start", "false");
476    }
477    private static String getAttribute (Element e, String attr, String def) {
478        return Environment.get(e.getAttributeValue(attr, def));
479    }
480
481    @SuppressWarnings("rawtypes")
482    public static void autoconfigure (Object obj, Configuration cfg) throws IllegalAccessException {
483        Class cc = obj.getClass();
484        do {
485            Field[] fields = cc.getDeclaredFields();
486            for (Field field : fields) {
487                if (field.isAnnotationPresent(Config.class)) {
488                    Config config = field.getAnnotation(Config.class);
489                    String v = cfg.get(config.value(), null);
490                    if (v != null) {
491                        if (!field.isAccessible())
492                            field.setAccessible(true);
493                        Class<?> c = field.getType();
494                        if (c.isAssignableFrom(String.class))
495                            field.set(obj, v);
496                        else if (c.isAssignableFrom(int.class) || c.isAssignableFrom(Integer.class))
497                            field.set(obj, cfg.getInt(config.value()));
498                        else if (c.isAssignableFrom(long.class) || c.isAssignableFrom(Long.class))
499                            field.set(obj, cfg.getLong(config.value()));
500                        else if (c.isAssignableFrom(double.class) || c.isAssignableFrom(Double.class))
501                            field.set(obj, cfg.getDouble(config.value()));
502                        else if (c.isAssignableFrom(boolean.class) || c.isAssignableFrom(Boolean.class))
503                            field.set(obj, cfg.getBoolean(config.value()));
504                        else if (c.isEnum())
505                            field.set(obj, Enum.valueOf((Class<Enum>) c, v));
506                        else if (c.isArray()) {
507                            Class<?> ct = c.getComponentType();
508                            if (ct.isAssignableFrom(String.class))
509                                field.set(obj, cfg.getAll(config.value()));
510                            else if (ct.isAssignableFrom(int.class) || ct.isAssignableFrom(Integer.class))
511                                field.set(obj, cfg.getInts(config.value()));
512                            else if (ct.isAssignableFrom(long.class) || ct.isAssignableFrom(Long.class))
513                                field.set(obj, cfg.getLongs(config.value()));
514                            else if (ct.isAssignableFrom(double.class) || ct.isAssignableFrom(Double.class))
515                                field.set(obj, cfg.getDoubles(config.value()));
516                        }
517                    }
518                }
519            }
520            cc = cc.getSuperclass();
521        } while (!cc.equals(Object.class));
522    }
523
524    /**
525     * Decorates an {@link Element} by replacing its attributes, and content {@link Environment} properties references.
526     * @param e The element being decorated.
527     * @return The modified element, it is modified in place, but it is returned to ease method chaining or call composition.
528     */
529    public static Element expandEnvProperties(Element e) {
530       expandEnvProperties(e, Environment.getEnvironment());
531       return e;
532    }
533
534    public static ExecutorService executorService(boolean virtual) {
535        return virtual ?
536            Executors.newVirtualThreadPerTaskExecutor() :
537            Executors.newThreadPerTaskExecutor(
538              Thread.ofPlatform().inheritInheritableThreadLocals(true)
539                .factory()
540            );
541    }
542
543    /**
544     * Recursively replaces {@link Environment} properties in an element's attributes, its content and its children.
545     * Properties are replaced in place.
546     * @param e The element in which properties are being replaced.
547     * @param env The {@link Environment}'s singleton instance.
548     */
549    private static void expandEnvProperties(Element e, Environment env) {
550        if (Boolean.parseBoolean(e.getAttributeValue("verbatim"))) return;
551        for (org.jdom2.Attribute attr : e.getAttributes()) {
552            String value = attr.getValue();
553            attr.setValue(env.getProperty(value, value));
554        }
555        for (Content child : e.getContent()) {
556            if (child instanceof Element) {
557                expandEnvProperties((Element) child, env);
558            } else if (child instanceof Text text) {
559                String textValue = text.getText();
560                text.setText(env.getProperty(textValue, textValue));
561            }
562        }
563    }
564
565}