/* * Copyright (c) 2003-2012 Fred Hutchinson Cancer Research Center * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.swixml; import org.jdom.Document; import org.jdom.input.SAXBuilder; import javax.swing.*; import java.awt.*; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.io.*; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.net.URL; import java.security.AccessControlException; import java.util.*; import java.util.List; /** * The SwingEngine class is the rendering engine able to convert an XML descriptor into a java.swing UI. * <p/> * <img src="doc-files/swixml_1_0.png" ALIGN="center"> * </p> * @author <a href="mailto:wolf@paulus.com">Wolf Paulus</a> * @version $Revision: 1.4 $ */ public class SwingEngine { // // Static Constants // /** * Mac OSX identifier in System.getProperty(os.name) */ public static final String MAC_OSX_OS_NAME = "mac os x"; /** * Mac OSX locale variant to localize strings like quit etc. */ public static final String MAC_OSX_LOCALE_VARIANT = "mac"; /** * Debug / Release Mode */ public static final boolean DEBUG_MODE = false; /** * XML Error */ private static final String XML_ERROR_MSG = "Invalid SwiXML Descriptor."; /** * IO Error Message. */ private static final String IO_ERROR_MSG = "Resource could not be found "; /** * Mapping Error Message. */ private static final String MAPPING_ERROR_MSG = " could not be mapped to any Object and remained un-initialized."; // // Static Member Variables // /** * main frame */ private static Frame appFrame; /** * static resource bundle */ private static String default_resource_bundle_name = null; /** * static locale */ private static Locale default_locale = Locale.getDefault(); /** * Check is currently running on a Mac */ private static boolean MAC_OSX = false; /** * static Mac OS X Support, set to true to support Mac UI specialties */ private static boolean MAC_OSX_SUPPORTED = true; // // Static Initializer // /** display the swing release version to system out. */ static { //dhmay commenting out the next line for msInspect because it's really annoying to have //it appear every time we start up // System.out.println("SwixML @version@"); try { MAC_OSX = System.getProperty("os.name").toLowerCase().startsWith(SwingEngine.MAC_OSX_OS_NAME); } catch (Exception e) { MAC_OSX = false; } } // // Member Variables // /** * Swixml Parser. */ private Parser parser = new Parser(this); /** * Client object hosting the swingengine, alternative to extending the SwinEngine Class */ private Object client; /** * Root Component for the rendered swing UI. */ private Container root; /** * Swing object map, contains only those object that were given an id attribute. */ private Map idmap = new HashMap(); /** * Flattened Swing object tree, contains all object, even the ones without an id. */ private Collection components = null; /** * access to taglib to let overwriting class add and remove tags. */ private Localizer localizer = new Localizer(); // // Private Constants // /** * Classload to load resources */ private final TagLibrary taglib = SwingTagLibrary.getInstance(); /** * Localizer, setup by parameters found in the xml descriptor. */ protected ClassLoader cl = this.getClass().getClassLoader(); /** * Default ctor for a SwingEngine. */ public SwingEngine() { this.client = this; this.setLocale(SwingEngine.default_locale); this.getLocalizer().setResourceBundle(SwingEngine.default_resource_bundle_name); try { if (SwingEngine.isMacOSXSupported() && SwingEngine.isMacOSX()) { // Use apple's ScreenMenuBar instead of the MS-Window style // application's own menu bar System.setProperty("com.apple.macos.useScreenMenuBar", "true"); System.setProperty("apple.laf.useScreenMenuBar", "true"); // Don't let the growbox intrude other widgets System.setProperty("apple.awt.showGrowBox", "true"); System.setProperty("com.apple.mrj.application.growbox.intrudes", "false"); } } catch (AccessControlException e) { // intentionally empty } } /** * Constructor to be used if the SwingEngine is not extend but used through object composition. * * @param client <code>Object</code> owner of this instance */ public SwingEngine(Object client) { this(); this.client = client; } /** * Constructs a new SwingEngine, rendering the provided XML into a javax.swing UI * * @param resource <code>String</code> */ public SwingEngine(final String resource) { this(SwingEngine.class.getClassLoader(), resource); } /** * Constructs a new SwingEngine, rendering the provided XML into a javax.swing UI * * @param resource <code>String</code> * @deprecated */ public SwingEngine(ClassLoader cl, final String resource) { this(); this.setClassLoader(cl); Reader reader = null; try { InputStream in = cl.getResourceAsStream(resource); if (in == null) { throw new IOException(IO_ERROR_MSG + resource); } reader = new InputStreamReader(in); render(reader); } catch (Exception e) { if (SwingEngine.DEBUG_MODE) System.err.println(e); } finally { try { reader.close(); } catch (Exception e) { // intentionally empty } } } /** * Gets the parsing of the XML started. * * @param url <code>URL</code> url pointing to an XML descriptor * @return <code>Object</code>- instanced swing object tree root * @throws Exception */ public Container render(final URL url) throws Exception { Reader reader = null; Container obj = null; try { InputStream in = url.openStream(); if (in == null) { throw new IOException(IO_ERROR_MSG + url.toString()); } reader = new InputStreamReader(in); obj = render(reader); } finally { try { reader.close(); } catch (Exception ex) { // intentionally empty } } return obj; } /** * Gets the parsing of the XML file started. * * @param resource <code>String</code> xml-file path info * @return <code>Object</code>- instanced swing object tree root */ public Container render(final String resource) throws Exception { Reader reader = null; Container obj = null; try { InputStream in = cl.getResourceAsStream(resource); if (in == null) { throw new IOException(IO_ERROR_MSG + resource); } reader = new InputStreamReader(in); obj = render(reader); } finally { try { reader.close(); } catch (Exception ex) { // intentionally empty } } return obj; } /** * Gets the parsing of the XML file started. * * @param xml_file <code>File</code> xml-file * @return <code>Object</code>- instanced swing object tree root */ public Container render(final File xml_file) throws Exception { if (xml_file == null) { throw new IOException(); } return render(new FileReader(xml_file)); } /** * Gets the parsing of the XML file started. * * @param xml_reader <code>Reader</code> xml-file path info * @return <code>Object</code>- instanced swing object tree root */ public Container render(final Reader xml_reader) throws Exception { if (xml_reader == null) { throw new IOException(); } try { return render(new SAXBuilder().build(xml_reader)); } catch (org.xml.sax.SAXParseException e) { System.err.println(e); } catch (org.jdom.input.JDOMParseException e) { System.err.println(e); } throw new Exception(SwingEngine.XML_ERROR_MSG); } /** * Gets the parsing of the XML file started. * * @param jdoc <code>Document</code> xml gui descritptor * @return <code>Object</code>- instanced swing object tree root */ public Container render(final Document jdoc) throws Exception { idmap.clear(); try { root = (Container) parser.parse(jdoc); } catch (Exception e) { if (SwingEngine.DEBUG_MODE) System.err.println(e); throw (e); } // reset components collection components = null; // initialize all client fields with UI components by their id mapMembers(client); if (Frame.class.isAssignableFrom(root.getClass())) { SwingEngine.setAppFrame((Frame) root); } return root; } /** * Inserts swing object rendered from an XML document into the given container. * <p/> * <pre> * Differently to the render methods, insert does NOT consider the root node of the XML document. * </pre> * <pre> * <b>NOTE:</b><br>insert() does NOT clear() the idmap before rendering. * Therefore, if this SwingEngine's parser was used before, the idmap still * contains (key/value) pairs (id, JComponent obj. references).<br>If insert() is NOT * used to insert in a previously (with this very SwingEngine) rendered UI, * it is highly recommended to clear the idmap: * <br> * <div> * <code>mySwingEngine.getIdMap().clear()</code> * </div> * </pre> * * @param url <code>URL</code> url pointing to an XML descriptor * * @param container <code>Container</code> target, the swing obj, are added to. * @throws Exception */ public void insert(final URL url, final Container container) throws Exception { Reader reader = null; try { InputStream in = url.openStream(); if (in == null) { throw new IOException(IO_ERROR_MSG + url.toString()); } reader = new InputStreamReader(in); insert(reader, container); } finally { try { reader.close(); } catch (Exception ex) { // intentionally empty } } } /** * Inserts swing objects rendered from an XML reader into the given container. * <p/> * <pre> * Differently to the render methods, insert does NOT consider the root node of the XML document. * </pre> * <pre> * <b>NOTE:</b><br>insert() does NOT clear() the idmap before rendering. * Therefore, if this SwingEngine's parser was used before, the idmap still * contains (key/value) pairs (id, JComponent obj. references).<br>If insert() is NOT * used to insert in a previously (with this very SwingEngine) rendered UI, it is highly * recommended to clear the idmap: * <br> * <div> * <code>mySwingEngine.getIdMap().clear()</code> * </div> * </pre> * * @param reader <code>Reader</code> xml-file path info * @param container <code>Container</code> target, the swing obj, are added to. * @throws Exception */ public void insert(final Reader reader, final Container container) throws Exception { if (reader == null) { throw new IOException(); } insert(new SAXBuilder().build(reader), container); } /** * Inserts swing objects rendered from an XML reader into the given container. * <p/> * <pre> * Differently to the render methods, insert does NOT consider the root node of the XML document. * </pre> * <pre> * <b>NOTE:</b><br>insert() does NOT clear() the idmap before rendering. * Therefore, if this SwingEngine's parser was used before, the idmap still * contains (key/value) pairs (id, JComponent obj. references).<br> * If insert() is NOT used to insert in a previously (with this very SwingEngine) * rendered UI, it is highly recommended to clear the idmap: * <br> * <div> * <code>mySwingEngine.getIdMap().clear()</code> * </div> * </pre> * * @param resource <code>String</code> xml-file path info * @param container <code>Container</code> target, the swing obj, are added to. * @throws Exception */ public void insert(final String resource, final Container container) throws Exception { Reader reader = null; try { InputStream in = cl.getResourceAsStream(resource); if (in == null) { throw new IOException(IO_ERROR_MSG + resource); } reader = new InputStreamReader(in); insert(reader, container); } finally { try { reader.close(); } catch (Exception ex) { // intentionally empty } } } /** * Inserts swing objects rendered from an XML document into the given container. * <p/> * <pre> * Differently to the parse methods, insert does NOT consider the root node of the XML document. * </pre> * <pre> * <b>NOTE:</b><br>insert() does NOT clear() the idmap before rendering. * Therefore, if this SwingEngine's parser was used before, the idmap still * contains (key/value) pairs (id, JComponent obj. references).<br> * If insert() is NOT * used to insert in a previously (with this very SwingEngine) rendered UI, * it is highly recommended to clear the idmap: * <br> * <div> * <code>mySwingEngine.getIdMap().clear()</code> * </div> * </pre> * * @param jdoc <code>Document</code> xml-doc path info * @throws Exception */ public void insert(final Document jdoc, final Container container) throws Exception { root = container; try { parser.parse(jdoc, container); } catch (Exception e) { if (SwingEngine.DEBUG_MODE) System.err.println(e); throw (e); } // reset components collection components = null; // initialize all client fields with UI components by their id mapMembers(client); } /** * Sets the SwingEngine's global resource bundle name, to be used by all SwingEngine instances. This name can be * overwritten however for a single instance, if a <code>bundle</code> attribute is places in the root tag of an XML * descriptor. * * @param bundlename <code>String</code> the resource bundle name. */ public static void setResourceBundleName(String bundlename) { SwingEngine.default_resource_bundle_name = bundlename; } /** * Sets the SwingEngine's global locale, to be used by all SwingEngine instances. This locale can be overwritten * however for a single instance, if a <code>locale</code> attribute is places in the root tag of an XML descriptor. * * @param locale <code>Locale</code> */ public static void setDefaultLocale(Locale locale) { SwingEngine.default_locale = locale; } /** * Sets the SwingEngine's global application frame variable, to be used as a parent for all child dialogs. * * @param frame <code>Object</code> the parent for all future dialogs. */ public static void setAppFrame(Frame frame) { if (frame != null) { if (SwingEngine.appFrame == null) { SwingEngine.appFrame = frame; } } } /** * @return <code>Frame</code> a parent for all dialogs. */ public static Frame getAppFrame() { return SwingEngine.appFrame; } /** * Returns the object which instantiated this SwingEngine. * * @return <code>Objecy</code> SwingEngine client object * <p/> * <pre><b>Note:</b><br> * This is the object used through introspection the actions and fileds are set. * </pre> */ public Object getClient() { return client; } /** * Returns the root component of the generated Swing UI. * * @return <code>Component</code>- the root component of the javax.swing ui */ public Container getRootComponent() { return root; } /** * Returns an Iterator for all parsed GUI components. * * @return <code>Iterator</code> GUI components itearator */ public Iterator getAllComponentItertor() { if (components == null) { traverse(root, components = new ArrayList()); } return components.iterator(); } /** * Returns an Iterator for id-ed parsed GUI components. * * @return <code>Iterator</code> GUI components itearator */ public Iterator getIdComponentItertor() { return idmap.values().iterator(); } /** * Returns the id map, containing all id-ed parsed GUI components. * * @return <code>Map</code> GUI components map */ public Map getIdMap() { return idmap; } /** * Removes all un-displayable compontents from the id map and deletes the components collection (for recreation at the * next request). * <p/> * <pre> * A component is made undisplayable either when it is removed from a displayable containment hierarchy or when its * containment hierarchy is made undisplayable. A containment hierarchy is made undisplayable when its ancestor * window * is disposed. * </pre> * * @return <code>int</code> number of removed componentes. */ public int cleanup() { List zombies = new ArrayList(); Iterator it = idmap.keySet().iterator(); while (it != null && it.hasNext()) { Object key = it.next(); Object obj = idmap.get(key); if (obj instanceof Component && !((Component) obj).isDisplayable()) { zombies.add(key); } } for (int i = 0; i < zombies.size(); i++) { idmap.remove(zombies.get(i)); } components = null; return zombies.size(); } /** * Removes the id from the internal from the id map, to make the given id available for re-use. * * @param id <code>String</code> assigned name */ public void forget(final String id) { idmap.remove(id); } /** * Returns the UI component with the given name or null. * * @param id <code>String</code> assigned name * @return <code>Component</code>- the GUI component with the given name or null if not found. */ public Component find(final String id) { Object obj = idmap.get(id); if (obj != null && !Component.class.isAssignableFrom(obj.getClass())) { obj = null; } return (Component) obj; } /** * Sets the locale to be used during parsing / String conversion * * @param l <code>Locale</code> */ public void setLocale(Locale l) { if (SwingEngine.isMacOSXSupported() && SwingEngine.isMacOSX()) { l = new Locale(l.getLanguage(), l.getCountry(), SwingEngine.MAC_OSX_LOCALE_VARIANT); } this.localizer.setLocale(l); } /** * Sets the ResourceBundle to be used during parsing / String conversion * * @param bundlename <code>String</code> */ public void setResourceBundle(String bundlename) { this.localizer.setResourceBundle(bundlename); } /** * @return <code>Localizer</code>- the Localizer, which is used for localization. */ public Localizer getLocalizer() { return localizer; } /** * @return <code>TagLibrary</code>- the Taglibray to insert custom tags. * <p/> * <pre><b>Note:</b>ConverterLibrary and TagLibray need to be set up before rendering is called. * </pre> */ public TagLibrary getTaglib() { return taglib; } /** * Sets a classloader to be used for all <i>getResourse..()</i> and <i> loadClass()</i> calls. If no class loader is * set, the SwingEngine's loader is used. * * @param cl <code>ClassLoader</code> * @see ClassLoader#loadClass * @see ClassLoader#getResource */ public void setClassLoader(ClassLoader cl) { this.cl = cl; this.localizer.setClassLoader(cl); } /** * @return <code>ClassLoader</code>- the Classloader used for all <i> getResourse..()</i> and <i>loadClass()</i> * calls. */ public ClassLoader getClassLoader() { return cl; } /** * Recursively Sets an ActionListener * <p/> * <pre> * Backtracking algorithm: if al was set for a child component, its not being set for its parent * </pre>. * * @param c <code>Component</code> start component * @param al <code>ActionListener</code> */ public boolean setActionListener(final Component c, final ActionListener al) { boolean b = false; if (c != null) { if (Container.class.isAssignableFrom(c.getClass())) { final Component[] s = ((Container) c).getComponents(); for (int i = 0; i < s.length; i++) { b = b | setActionListener(s[i], al); } } if (!b) { if (JMenu.class.isAssignableFrom(c.getClass())) { final JMenu m = (JMenu) c; final int k = m.getItemCount(); for (int i = 0; i < k; i++) { b = b | setActionListener(m.getItem(i), al); } } else if (AbstractButton.class.isAssignableFrom(c.getClass())) { ((AbstractButton) c).addActionListener(al); b = true; } } } return b; } /** * Walks the whole tree to add all components into the <code>components<code> collection. * * @param c <code> Component</code> recursive start component. * <p> * Note:There is another collection available that only tracks * those object that were provided with an <em>id</em>attribute, which hold an unique id * </p> */ public Iterator getDescendants(final Component c) { List list = new ArrayList(12); SwingEngine.traverse(c, list); return list.iterator(); } /** * Introspects the given object's class and initializes its non-transient fields with objects that have been instanced * during parsing. Mappping happens based on type and field name: the fields name has to be equal to the tag id, * psecified in the XML descriptor. The fields class has to be assignable (equals or super class..) from the class * that was used to instance the tag. * * @param obj <code>Object</code> target object to be mapped with instanced tags */ protected void mapMembers(Object obj) { if (obj != null) { mapMembers(obj, obj.getClass()); } } private void mapMembers(Object obj, Class cls) { if (obj != null && cls != null && !Object.class.equals(cls)) { Field[] flds = cls.getDeclaredFields(); // // loops through class' declared fields and try to find a matching widget. // for (int i = 0; i < flds.length; i++) { Object widget = idmap.get(flds[i].getName()); if (widget != null) { // field and object type need to be compatible and field must not be declared Transient if (flds[i].getType().isAssignableFrom(widget.getClass()) && !Modifier.isTransient(flds[i].getModifiers())) { try { boolean accessible = flds[i].isAccessible(); flds[i].setAccessible(true); flds[i].set(obj, widget); flds[i].setAccessible(accessible); } catch (IllegalArgumentException e) { // intentionally empty } catch (IllegalAccessException e) { // intentionally empty } } } // // If an intended mapping didn't work out the objects member would remain un-initialized. // To prevent this, we try to instantiate with a default ctor. // if (flds[i] == null) { if (!SwingEngine.DEBUG_MODE) { try { flds[i].set(obj, flds[i].getType().newInstance()); } catch (IllegalArgumentException e) { // intentionally empty } catch (IllegalAccessException e) { // intentionally empty } catch (InstantiationException e) { // intentionally empty } } else { // SwingEngine.DEBUG_MODE) System.err.println(flds[i].getType() + " : " + flds[i].getName() + SwingEngine.MAPPING_ERROR_MSG); } } } // Since getDeclaredFields() only works on the class itself, not the super class, // we need to make this recursive down to the object.class mapMembers(obj, cls.getSuperclass()); } } /** * Walks the whole tree to add all components into the <code>components<code> collection. * * @param c <code>Component</code> recursive start component. * <p> * Note:There is another collection available that only tracks * those object that were provided with an <em>id</em>attribute, which hold an unique id * </p> */ protected static void traverse(final Component c, Collection collection) { if (c != null) { collection.add(c); if (c instanceof JMenu) { final JMenu m = (JMenu) c; final int k = m.getItemCount(); for (int i = 0; i < k; i++) { traverse(m.getItem(i), collection); } } else if (c instanceof Container) { final Component[] s = ((Container) c).getComponents(); for (int i = 0; i < s.length; i++) { traverse(s[i], collection); } } } } /** * Enables or disables support of Mac OS X GUIs * * @param osx <code>boolean</code> */ public static void setMacOSXSuport(boolean osx) { SwingEngine.MAC_OSX_SUPPORTED = osx; } /** * Indicates state of Mac OS X support (default is true = ON). * * @return <code>boolean</code>- indicating MacOS support is enabled */ public static boolean isMacOSXSupported() { return SwingEngine.MAC_OSX_SUPPORTED; } /** * Indicates if currently running on Mac OS X * * @return <code>boolean</code>- indicating if currently running on a MAC */ public static boolean isMacOSX() { return SwingEngine.MAC_OSX; } /** * Displays the GUI during a RAD session. If the root component is neither a JFrame nor a JDialog, the a JFrame is * instantiated and the root is added into the new frames contentpane. */ public void test() { WindowListener wl = new WindowAdapter() { public void windowClosing(WindowEvent e) { super.windowClosing(e); System.exit(0); } }; if (root != null) { if (JFrame.class.isAssignableFrom(root.getClass()) || JDialog.class.isAssignableFrom(root.getClass())) { ((Window) root).addWindowListener(wl); root.setVisible(true); } else { JFrame jf = new JFrame("SwiXml Test"); jf.getContentPane().add(root); jf.pack(); jf.addWindowListener(wl); jf.setVisible(true); } } } }