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