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
021import org.jdom2.Element;
022import org.jpos.core.Configurable;
023import org.jpos.core.Configuration;
024import org.jpos.core.ConfigurationException;
025import org.jpos.util.*;
026
027import java.beans.BeanInfo;
028import java.beans.Introspector;
029import java.beans.PropertyDescriptor;
030import java.io.ByteArrayOutputStream;
031import java.io.Closeable;
032import java.io.PrintStream;
033import java.lang.reflect.Method;
034import java.net.URL;
035import java.util.Iterator;
036import java.util.concurrent.ScheduledThreadPoolExecutor;
037
038/**
039 * Base class providing lifecycle management and configuration support for Q2 beans (QBean components).
040 * @author <a href="mailto:taherkordy@dpi2.dpi.net.ir">Alireza Taherkordi</a>
041 * @author <a href="mailto:apr@cs.com.uy">Alejandro P. Revilla</a>
042 */
043public class QBeanSupport
044    implements QBean, QPersist, QBeanSupportMBean, Configurable
045{
046    Element persist;
047    int state;
048    Q2 server;
049    final Object modifyLock = new Object();
050    boolean modified;
051    String name;
052    String configuredRealm;
053    /** Logger associated with this bean. */
054    protected Log log;
055    /** Configuration applied to this bean by the container. */
056    protected Configuration cfg;
057    /** Lazily-allocated scheduler shared with subclasses; see {@link #getScheduledThreadPoolExecutor()}. */
058    protected ScheduledThreadPoolExecutor scheduledThreadPoolExecutor;
059
060    /** Default constructor. */
061    public QBeanSupport () {
062        super();
063        setLogger (Q2.LOGGER_NAME);
064        state = -1;
065    }
066
067    @Override
068    public void setServer (Q2 server) {
069        this.server = server;
070    }
071
072    @Override
073    public Q2 getServer () {
074        return server;
075    }
076    /**
077     * Returns the {@link QFactory} associated with the hosting Q2 server.
078     *
079     * @return the server's QFactory
080     */
081    public QFactory getFactory () {
082        return getServer().getFactory ();
083    }
084
085    @Override
086    public void setName (String name) {
087        if (this.name == null)
088            this.name = name;
089        syncLogConfiguration();
090        setModified (true);
091    }
092
093    @Override
094    public void setLogger (String loggerName) {
095        log = Log.getLog (loggerName, resolveRealm());
096        syncLogConfiguration();
097        setModified (true);
098    }
099
100    @Override
101    public void setRealm (String realm) {
102        configuredRealm = realm;
103        syncLogConfiguration();
104    }
105
106    @Override
107    public String getRealm() {
108        return log != null ? log.getRealm() : null;
109    }
110
111    @Override
112    public String getLogger () {
113        return log != null ? log.getLogger().getName() : null;
114    }
115
116    /**
117     * Returns the {@link Log} used by this bean.
118     *
119     * @return the configured log instance, or {@code null} if logging has not been initialized
120     */
121    public Log getLog () {
122        return log;
123    }
124
125    /**
126     * Returns the default realm for this bean; subclasses may override.
127     * @return default realm string, or null
128     */
129    protected String defaultRealm() {
130        return null;
131    }
132
133    @Override
134    public String getName () {
135        return name;
136    }
137
138    @Override
139    public void init () {
140        if (state == -1) {
141            setModified (false);
142            try {
143                initService();
144                state = QBean.STOPPED;
145            } catch (Throwable t) {
146                log.warn ("init", t);
147            }
148        }
149    }
150
151    @Override
152    public synchronized void start() {
153        if (state != QBean.DESTROYED &&
154            state != QBean.STOPPED   &&
155            state != QBean.FAILED)
156           return;
157
158        this.state = QBean.STARTING;
159
160        try {
161           startService();
162        } catch (Throwable t) {
163           state = QBean.FAILED;
164           log.warn ("start", t);
165           return;
166        }
167        state = QBean.STARTED;
168    }
169
170    @Override
171    public synchronized void stop () {
172        if (state != QBean.STARTED)
173           return;
174        state = QBean.STOPPING;
175        try {
176           stopService();
177        } catch (Throwable t) {
178           state = QBean.FAILED;
179           log.warn ("stop", t);
180           return;
181        }
182        state = QBean.STOPPED;
183    }
184
185    @Override
186    public void destroy () {
187        if (state == QBean.DESTROYED)
188           return;
189        if (state != QBean.STOPPED)
190           stop();
191
192        if (scheduledThreadPoolExecutor != null) {
193            scheduledThreadPoolExecutor.shutdown();
194            scheduledThreadPoolExecutor = null;
195        }
196        try {
197           destroyService();
198        }
199        catch (Throwable t) {
200           log.warn ("destroy", t);
201        }
202        state = QBean.DESTROYED;
203    }
204
205    @Override
206    public int getState () {
207        return state;
208    }
209
210    @Override
211    public URL[] getLoaderURLS() {
212        return server.getLoader().getURLs();
213    }
214
215    @Override
216    public QClassLoader getLoader() {
217        return server.getLoader();
218    }
219
220    @Override
221    public String getStateAsString () {
222        return state >= 0 ? stateString[state] : "Unknown";
223    }
224
225    /**
226     * Sets the lifecycle state of this bean.
227     *
228     * @param state one of the QBean state constants
229     */
230    public void setState (int state) {
231        this.state = state;
232    }
233
234    @Override
235    public void setPersist (Element persist) {
236        this.persist = persist ;
237    }
238
239    @Override
240    public Element getPersist () {
241        setModified (false);
242        return persist;
243    }
244
245    /**
246     * Marks this bean's persistent state as modified or in sync.
247     *
248     * @param modified {@code true} if the bean has unsaved changes
249     */
250    public void setModified (boolean modified) {
251        synchronized (this.modifyLock) {
252            this.modified = modified;
253        }
254    }
255
256    @Override
257    public boolean isModified () {
258        synchronized (this.modifyLock) {
259            return modified;
260        }
261    }
262
263    /**
264     * Indicates whether this bean is currently starting or running.
265     *
266     * @return {@code true} if the state is {@link QBean#STARTING} or {@link QBean#STARTED}
267     */
268    public boolean running () {
269        return state == QBean.STARTING || state == QBean.STARTED;
270    }
271
272    @Override
273    public void setConfiguration (Configuration cfg)
274      throws ConfigurationException
275    {
276        this.cfg = cfg;
277    }
278    /**
279     * Returns the configuration applied to this bean.
280     *
281     * @return the bean's {@link Configuration}, or {@code null} if not yet configured
282     */
283    public Configuration getConfiguration () {
284        return cfg;
285    }
286
287    private String resolveRealm() {
288        if (configuredRealm != null)
289            return configuredRealm;
290        String defaultRealm = defaultRealm();
291        if (defaultRealm != null)
292            return defaultRealm;
293        return name != null ? name : getClass().getName();
294    }
295
296    private void syncLogConfiguration() {
297        if (log == null)
298            return;
299        log.setRealm(resolveRealm());
300        log.removeDefaultTag("component");
301        if (configuredRealm == null && defaultRealm() != null && name != null)
302            log.setDefaultTag("component", name);
303    }
304
305    /**
306     * Returns a textual dump of this bean's state.
307     *
308     * <p>If the bean implements {@link Loggeable}, its {@code dump} output is returned;
309     * otherwise this delegates to {@link #toString()}.
310     *
311     * @return a string representation suitable for diagnostics
312     */
313    public String getDump () {
314        if (this instanceof Loggeable) {
315            ByteArrayOutputStream baos = new ByteArrayOutputStream();
316            PrintStream p = new PrintStream(baos);
317            ((Loggeable)this).dump(p, "");
318            return baos.toString();
319        }
320        return toString();
321    }
322    /**
323     * Called during the init lifecycle phase; subclasses may override.
324     * @throws Exception on error
325     */
326    protected void initService()    throws Exception {}
327    /**
328     * Called during the start lifecycle phase; subclasses may override.
329     * @throws Exception on error
330     */
331    protected void startService()   throws Exception {}
332    /**
333     * Called during the stop lifecycle phase; subclasses may override.
334     * @throws Exception on error
335     */
336    protected void stopService()    throws Exception {}
337    /**
338     * Called during the destroy lifecycle phase; subclasses may override.
339     * @throws Exception on error
340     */
341    protected void destroyService() throws Exception {}
342
343    /**
344     * Lazily creates and returns a {@link ScheduledThreadPoolExecutor} for use by subclasses.
345     *
346     * @return shared scheduled executor for this bean
347     */
348    protected synchronized ScheduledThreadPoolExecutor getScheduledThreadPoolExecutor() {
349        if (scheduledThreadPoolExecutor == null)
350            scheduledThreadPoolExecutor = ConcurrentUtil.newScheduledThreadPoolExecutor();
351        return scheduledThreadPoolExecutor;
352    }
353
354    /**
355     * Builds a JDOM {@link Element} reflecting the bean's persistent attributes,
356     * driven by the JavaBean introspection of {@code mbeanClass}.
357     *
358     * @param name the root element name
359     * @param mbeanClass the MBean interface whose properties drive serialization
360     * @return populated element ready for persistence
361     */
362    protected Element createElement (String name, Class mbeanClass) {
363        Element e = new Element (name);
364        Element classPath = persist != null ?
365            persist.getChild ("classpath") : null;
366        if (classPath != null)
367            e.addContent (classPath);
368        e.setAttribute ("class", getClass().getName());
369        if (!e.getName().equals (getName ()))
370            e.setAttribute ("name", getName());
371        String loggerName = getLogger();
372        if (loggerName != null)
373            e.setAttribute ("logger", loggerName);
374
375        try {
376            BeanInfo info = Introspector.getBeanInfo (mbeanClass);
377            PropertyDescriptor[] desc = info.getPropertyDescriptors();
378            for (PropertyDescriptor aDesc : desc) {
379                if (aDesc.getWriteMethod() != null) {
380                    Method read = aDesc.getReadMethod();
381                    Object obj = read.invoke(this);
382                    String type = read.getReturnType().getName();
383                    if ("java.lang.String".equals(type))
384                        type = null;
385
386                    addAttr(e, aDesc.getName(), obj, type);
387                }
388            }
389        } catch (Exception ex) {
390            log.warn ("get-persist", ex);
391        }
392        return e;
393    }
394    /**
395     * Appends an {@code <attr>} child element with name/type/value to {@code e}.
396     *
397     * @param e parent element
398     * @param name attribute name
399     * @param obj value (rendered via {@code toString()}; {@code null} becomes the literal "null")
400     * @param type optional Java type hint, or {@code null} to omit
401     */
402    protected void addAttr (Element e, String name, Object obj, String type) {
403        String value = obj == null ? "null" : obj.toString();
404        Element attr = new Element ("attr");
405        attr.setAttribute ("name", name);
406        if (type != null)
407            attr.setAttribute ("type", type);
408        attr.setText (value);
409        e.addContent (attr);
410    }
411    /**
412     * Iterates over top-level {@code <attr>} children of the persisted element.
413     *
414     * @return iterator over the persisted attribute elements
415     */
416    protected Iterator getAttrs () {
417        return getPersist().getChildren ("attr").iterator();
418    }
419    /**
420     * Iterates over {@code <attr>} children nested under the given parent element name.
421     *
422     * @param parent name of the parent element under {@link #getPersist()}
423     * @return iterator over the nested attribute elements
424     */
425    protected Iterator getAttrs (String parent) {
426        return getPersist().getChild(parent).
427            getChildren("attr").iterator();
428    }
429    /**
430     * Updates the value of the named attribute within an iterator of {@code <attr>} elements.
431     *
432     * @param attrs iterator over candidate attribute elements
433     * @param name name of the attribute to update
434     * @param obj new value (rendered via {@code toString()}; {@code null} becomes "null")
435     */
436    protected void setAttr (Iterator attrs, String name, Object obj) {
437        String value = obj == null ? "null" : obj.toString ();
438        while (attrs.hasNext ()) {
439            Element e = (Element) attrs.next ();
440            if (name.equals (e.getAttributeValue ("name")))  {
441                e.setText(value);
442                break;
443            }
444        }
445    }
446    /**
447     * Iterates over {@code <property>} children nested under the given parent element name.
448     *
449     * @param parent name of the parent element under {@link #getPersist()}
450     * @return iterator over the nested property elements
451     */
452    protected Iterator getProperties (String parent) {
453        return getPersist().getChild (parent).
454               getChildren ("property").iterator();
455    }
456    /**
457     * Updates the value attribute of the named property within an iterator of {@code <property>} elements.
458     *
459     * @param props iterator over candidate property elements
460     * @param name name of the property to update
461     * @param value new value to assign
462     */
463    protected void setProperty (Iterator props, String name, String value) {
464        while (props.hasNext()) {
465            Element e = (Element) props.next();
466            if (name.equals (e.getAttributeValue ("name"))) {
467                e.setAttribute ("value", value);
468                break;
469            }
470        }
471    }
472    /**
473     * Returns the value of the named property from an iterator of {@code <property>} elements.
474     *
475     * @param props iterator over candidate property elements
476     * @param name name of the property to look up
477     * @return the property's value, or {@code null} if not found
478     */
479    protected String getProperty (Iterator props, String name) {
480        while (props.hasNext()) {
481            Element e = (Element) props.next();
482            if (name.equals (e.getAttributeValue ("name"))) {
483                return e.getAttributeValue ("value");
484            }
485        }
486        return null;
487    }
488    /**
489     * Closes a sequence of {@link Closeable} resources, logging any failures as a warning.
490     *
491     * @param closeables resources to close (each is closed independently of the others)
492     */
493    protected void close (Closeable... closeables) {
494        LogEvent evt = null;
495        for (Closeable c : closeables) {
496            try {
497                c.close();
498            } catch (Exception e) {
499                if (evt == null)
500                    evt = getLog().createWarn();
501                evt.addMessage(e);
502            }
503        }
504        if (evt != null)
505            Logger.log(evt);
506    }
507}