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.ui;
020
021import org.jdom2.Element;
022import org.jdom2.JDOMException;
023import org.jpos.util.Log;
024
025import javax.swing.*;
026import java.awt.*;
027import java.awt.event.ActionListener;
028import java.net.MalformedURLException;
029import java.net.URL;
030import java.util.*;
031
032/**
033 * Central controller for the jPOS Swing-based GUI; manages UI components and their factories.
034 * @author Alejandro Revilla
035 *
036 * <p>jPOS UI main class</p>
037 *
038 * @see UIFactory
039 *
040 * See src/examples/ui/* for usage details
041 */
042@SuppressWarnings({"unchecked", "deprecation"})
043public class UI implements UIFactory, UIObjectFactory {
044    JFrame mainFrame;
045    Map registrar, mapping;
046    Element config;
047    UIObjectFactory objFactory;
048
049    Log log;
050    boolean destroyed = false;
051    static final ResourceBundle classMapping;
052
053    static {
054        classMapping = ResourceBundle.getBundle(UI.class.getName());
055    }
056    /**
057     * Create a new UI object
058     */
059    public UI () {
060        super ();
061        registrar = new HashMap ();
062        mapping = new HashMap ();
063        setObjectFactory (this);
064    }
065    /**
066     * Creates a new UI object
067     * @param config configuration element
068     */
069    public UI (Element config) {
070        this ();
071        setConfig(config);
072    }
073    /**
074     * Assigns an object factory use to create new object instances.
075     * If no object factory is asigned, UI uses the default classloader
076     *
077     * @param objFactory reference to an Object Factory
078     */
079    public void setObjectFactory (UIObjectFactory objFactory) {
080        this.objFactory = objFactory;
081    }
082    /**
083     * Sets the XML configuration element for this UI.
084     * @param config the Configuration element
085     */
086    public void setConfig (Element config) {
087        this.config = config;
088    }
089    /**
090     * Sets the optional Log instance used for diagnostic output.
091     * @param log an optional Log instance
092     * @see org.jpos.util.Log
093     */
094    public void setLog (Log log) {
095        this.log = log;
096    }
097    /**
098     * Returns the Log instance, or null if none was set.
099     * @return the Log, or null
100     */
101    public Log getLog () {
102        return log;
103    }
104    /**
105     * UI uses a map to hold references to its components
106     * ("id" attribute)
107     *
108     * @return UI component registrar
109     */
110    public Map getRegistrar () {
111        return registrar;
112    }
113    /**
114     * Returns the component registered under the given id.
115     * @param id Component id ("id" configuration attribute)
116     * @return the Object or null
117     */
118    public Object get (String id) {
119        return registrar.get (id);
120    }
121   /**
122    * UI is itself a UIFactory. 
123    * This strategy is used to recursively instantiate components
124    * inside a container
125    * 
126    * @param ui reference to this UI instance
127    * @param e free form configuration Element
128    * @return JComponent
129    */
130    public JComponent create (UI ui, Element e) {
131        return create(e);
132    }
133    /**
134     * UIObjectFactory implementation.
135     * uses default classloader
136     * @param clazz the Clazzzz
137     * @return the Object
138     * @throws Exception if unable to instantiate
139     * @see #setLog
140     */
141    /**
142     * Instantiates an object by class name using the current thread's context class loader.
143     * @param clazz fully qualified class name
144     * @return new instance
145     * @throws Exception if the class cannot be found or instantiated
146     */
147    public Object newInstance (String clazz) throws Exception {
148        ClassLoader cl = Thread.currentThread().getContextClassLoader ();
149        Class type = cl.loadClass (clazz);
150        return type.newInstance();
151    }
152    /**
153     * Configures this UI from the stored XML element.
154     * @throws JDOMException on XML processing error
155     */
156    public void configure () throws JDOMException {
157        configure (config);
158    } 
159    /**
160     * reconfigure can be used in order to re-configure components
161     * inside a container (i.e. changing a panel in response to
162     * an event).
163     * @see org.jpos.ui.action.Redirect
164     *
165     * @param elementName the element name used as new configuration
166     * @param panelName panel ID (see "id" attribute)
167     */
168    public void reconfigure (String elementName, String panelName) {
169        Container c = 
170            panelName == null ? mainFrame.getContentPane() : (JComponent) get (panelName);
171        if (c != null) {
172            c.removeAll ();
173            c.add (
174                createComponent (config.getChild (elementName))
175            );
176            if (c instanceof JComponent) {
177                c.revalidate();
178            }
179            c.repaint ();
180        }
181    }
182    /**
183     * dispose this UI object
184     */
185    public void dispose () {
186     /* This is the right code for the dispose, but it freezes in
187        JVM running under WinXP (in linux went fine.. I didn't 
188        test it under other OS's)
189        (last version tested: JRE 1.5.0-beta2)
190  
191        if (mainFrame != null) {
192            // dumpComponent (mainFrame);
193            mainFrame.dispose ();
194     */
195        destroyed = true;
196
197        Iterator it = Arrays.asList(Frame.getFrames()).iterator();
198
199        while (it.hasNext()) {
200            JFrame jf = (JFrame) it.next();
201            removeComponent(jf);
202        }
203    }
204    /**
205     * Returns true if this UI object has been disposed.
206     * @return true if this UI object has been disposed and is no longer valid
207     */
208    public boolean isDestroyed () {
209        return destroyed;
210    }
211
212    /**
213     * Configures the UI from the given XML element.
214     * @param ui the root UI configuration element
215     * @throws JDOMException on XML processing error
216     */
217    protected void configure (Element ui) throws JDOMException {
218        setLookAndFeel (ui);
219        createMappings (ui);
220        createObjects (ui, "object");
221        createObjects (ui, "action");
222        if (!"ui".equals (ui.getName())) {
223            ui = ui.getChild ("ui");
224        }
225        if (ui != null) {
226            JFrame frame = initFrame (ui);
227            Element mb = ui.getChild ("menubar");
228            if (mb != null) 
229                frame.setJMenuBar (buildMenuBar (mb));
230
231            frame.setContentPane (
232                createComponent (ui.getChild ("components"))
233            );
234            if ("true".equals (ui.getAttributeValue ("full-screen"))) {
235                GraphicsDevice device = GraphicsEnvironment
236                                            .getLocalGraphicsEnvironment()
237                                            .getDefaultScreenDevice();
238                frame.setUndecorated (
239                    "true".equals (ui.getAttributeValue ("undecorated"))
240                );
241                device.setFullScreenWindow(frame);
242            } else {
243                frame.show ();
244            }
245        }
246    }
247
248    private void removeComponent (Component c) {
249        if (c instanceof Container) {
250            Container cont = (Container) c;
251            Component[] cc = cont.getComponents();
252
253            for (Component aCc : cc) {
254                removeComponent(aCc);
255            }
256            cont.removeAll();
257        }
258    }
259
260    // ##DEBUG##
261    private void dumpComponent (Component c) {
262        System.out.println (c.getClass().getName() + ":" + c.getBounds().getSize().toString());
263        if (c instanceof Container) {
264            Component[] cc = ((Container) c).getComponents();
265            for (Component aCc : cc) {
266                dumpComponent(aCc);
267            }
268        }
269    }
270
271    private JFrame initFrame (Element ui) {
272        Element caption = ui.getChild ("caption");
273        mainFrame = caption == null ?  
274            new JFrame () :
275            new JFrame (caption.getText());
276
277        JOptionPane.setRootFrame (mainFrame);
278
279        mainFrame.getContentPane().setLayout(new BorderLayout());
280
281        String close = ui.getAttributeValue ("close");
282
283        if ("false".equals (close))
284            mainFrame.setDefaultCloseOperation (JFrame.DO_NOTHING_ON_CLOSE);
285        else if ("exit".equals (close))
286            mainFrame.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
287
288        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
289        mainFrame.setSize(getDimension (ui, screenSize));
290        locateOnScreen(mainFrame);
291        return mainFrame;
292    }
293
294    private void locateOnScreen(Frame frame) {
295        Dimension paneSize   = frame.getSize();
296        Dimension screenSize = frame.getToolkit().getScreenSize();
297        frame.setLocation(
298                (screenSize.width - paneSize.width) / 2,
299                (screenSize.height - paneSize.height) / 2);
300    }
301    private JMenuBar buildMenuBar (Element ui) {
302        JMenuBar mb = new JMenuBar ();
303        Iterator iter = ui.getChildren("menu").iterator();
304        while (iter.hasNext()) 
305            mb.add (menu ((Element) iter.next()));
306
307        return mb;
308    }
309    private JMenu menu (Element m) {
310        JMenu menu = new JMenu (m.getAttributeValue ("id"));
311        setItemAttributes (menu, m);
312        Iterator iter = m.getChildren ().iterator();
313        while (iter.hasNext()) 
314            addMenuItem(menu, (Element) iter.next());
315        return menu;
316    }
317    private void addMenuItem (JMenu menu, Element m) {
318        String tag = m.getName ();
319
320        if ("menuitem".equals (tag)) {
321            JMenuItem item = new JMenuItem (m.getAttributeValue ("id"));
322            setItemAttributes (item, m);
323            menu.add (item);
324        } else if ("menuseparator".equals (tag)) {
325            menu.addSeparator ();
326        } else if ("button-group".equals (tag)) {
327            addButtonGroup (menu, m);
328        } else if ("check-box".equals (tag)) {
329            JCheckBoxMenuItem item = new JCheckBoxMenuItem (
330                m.getAttributeValue ("id")
331            );
332            setItemAttributes (item, m);
333            item.setState (
334                "true".equals (m.getAttributeValue ("state"))
335            );
336            menu.add (item);
337        } else if ("menu".equals (tag)) {
338            menu.add (menu (m));
339        }
340    }
341    private void addButtonGroup (JMenu menu, Element m) {
342        ButtonGroup group = new ButtonGroup();
343        Iterator iter = m.getChildren ("radio-button").iterator();
344        while (iter.hasNext()) {
345            addRadioButton (menu, group, (Element) iter.next());
346        }
347    }
348    private void addRadioButton (JMenu menu, ButtonGroup group, Element m) {
349        JRadioButtonMenuItem item = new JRadioButtonMenuItem
350            (m.getAttributeValue ("id"));
351        setItemAttributes (item, m);
352        item.setSelected (
353            "true".equals (m.getAttributeValue ("selected"))
354        );
355        group.add (item);
356        menu.add (item);
357    }
358    private Dimension getDimension (Element e, Dimension def) {
359        String w = e.getAttributeValue ("width");
360        String h = e.getAttributeValue ("height");
361
362        return new Dimension (
363           w != null ? Integer.parseInt (w) : def.width,
364           h != null ? Integer.parseInt (h) : def.height
365        );
366    }
367    private void setItemAttributes (AbstractButton b, Element e) 
368    {
369        String s = e.getAttributeValue ("accesskey");
370        if (s != null && s.length() == 1)
371            b.setMnemonic (s.charAt(0));
372
373        String icon = e.getAttributeValue ("icon");
374        if (icon != null) {
375            try {
376                b.setIcon (new ImageIcon (new URL (icon)));
377            } catch (MalformedURLException ex) {
378                ex.printStackTrace ();
379            }
380        }
381        b.setActionCommand (e.getAttributeValue ("command"));
382        String actionId = e.getAttributeValue ("action");
383        if (actionId != null) {
384            b.addActionListener ((ActionListener) get (actionId));
385        }
386    }
387    /**
388     * Applies the look-and-feel specified in the UI configuration element.
389     * @param ui the UI configuration element
390     */
391    protected void setLookAndFeel (Element ui) {
392        String laf = ui.getAttributeValue ("look-and-feel");
393        if (laf != null) {
394            try {
395                UIManager.setLookAndFeel (laf);
396            } catch (Exception e) {
397                warn (e);
398            }
399        }
400    }
401    private JComponent createComponent (Element e) {
402        if (e == null)
403            return new JPanel ();
404
405        JComponent component;
406        UIFactory factory = null;
407        String clazz = e.getAttributeValue ("class");
408        if (clazz == null) 
409            clazz = (String) mapping.get (e.getName());
410        if (clazz == null) {
411            try {
412                clazz = classMapping.getString (e.getName());
413            } catch (MissingResourceException ignored) {
414                // OK to happen on components handled by this factory
415            }
416        }
417        try {
418            if (clazz == null) 
419                factory = this;
420            else 
421                factory = (UIFactory) objFactory.newInstance (clazz.trim());
422
423            component = factory.create (this, e);
424            setSize (component, e);
425            if (component instanceof AbstractButton) {
426                AbstractButton b = (AbstractButton) component;
427                b.setActionCommand (e.getAttributeValue ("command"));
428                String actionId = e.getAttributeValue ("action");
429                if (actionId != null) {
430                    b.addActionListener ((ActionListener) get (actionId));
431                }
432            }
433            put (component, e);
434
435            Element script = e.getChild ("script");
436            if (script != null) 
437                component = doScript (component, script);
438
439            if ("true".equals (e.getAttributeValue ("scrollable")))
440                component = new JScrollPane (component);
441        } catch (Exception ex) {
442            warn ("Error instantiating class " + clazz);
443            warn (ex);
444            component = new JLabel ("Error instantiating class " + clazz);
445        }
446        return component;
447    }
448    /**
449     * Applies any script element to the component; default implementation is a no-op.
450     * @param component the target component
451     * @param e the script XML element
452     * @return the component after script application
453     */
454    protected JComponent doScript (JComponent component, Element e) {
455        return component;
456    }
457    private void setSize (JComponent c, Element e) {
458        String w = e.getAttributeValue ("width");
459        String h = e.getAttributeValue ("height");
460        Dimension d = c.getPreferredSize ();
461        double dw = d.getWidth ();
462        double dh = d.getHeight ();
463        if (w != null) 
464            dw = Double.parseDouble (w);
465        if (h != null) 
466            dh = Double.parseDouble (h);
467        if (w != null || h != null) {
468            d.setSize (dw, dh);
469            c.setPreferredSize (d);
470        }
471    }
472    /**
473     * Creates a Swing component from the given XML element descriptor.
474     * @param e the XML element describing the component
475     * @return the created JComponent, or {@code null} if none was produced
476     */
477    public JComponent create (Element e) {
478        JComponent component = null;
479
480        Iterator iter = e.getChildren().iterator();
481        for (int i=0; iter.hasNext(); i++) {
482            JComponent c = createComponent((Element) iter.next ());
483            if (i == 0)
484                component = c;
485            else if (i == 1) {
486                JPanel p = new JPanel ();
487                p.add (component);
488                p.add (c);
489                component = p;
490                put (component, e);
491            } else {
492                component.add (c);
493            }
494        }
495        return component;
496    }
497    /**
498     * Returns the application's main frame.
499     * @return the main JFrame
500     */
501    public JFrame getMainFrame() {
502        return mainFrame;
503    }
504    
505    private void createObjects (Element e, String name) {
506        Iterator iter = e.getChildren (name).iterator ();
507        while (iter.hasNext()) {
508            try {
509                Element ee = (Element) iter.next ();
510                String clazz = ee.getAttributeValue ("class");
511                Object obj = objFactory.newInstance (clazz.trim());
512                if (obj instanceof UIAware) {
513                    ((UIAware) obj).setUI (this, ee);
514                }
515                put (obj, ee);
516            } catch (Exception ex) {
517                warn (ex);
518            }
519        }
520    }
521    private void createMappings (Element e) {
522        Iterator iter = e.getChildren ("mapping").iterator ();
523        while (iter.hasNext()) {
524            try {
525                Element ee = (Element) iter.next ();
526                String name  = ee.getAttributeValue ("name");
527                String clazz = ee.getAttributeValue("factory");
528                mapping.put(name, clazz);
529            } catch (Exception ex) {
530                warn (ex);
531            }
532        }
533    }
534    /**
535     * Logs a warning.
536     * @param obj the warning object
537     */
538    protected void warn (Object obj) {
539        if (log != null)
540            log.warn (obj);
541    }
542    /**
543     * Logs a warning with an associated exception.
544     * @param obj the warning object
545     * @param ex the associated exception
546     */
547    protected void warn (Object obj, Exception ex) {
548        if (log != null)
549            log.warn (obj, ex);
550    }
551
552    private void put (Object obj, Element e) {
553        String id = e.getAttributeValue ("id");
554        if (id != null) {
555            registrar.put (id, obj);
556        }
557    }
558}
559