Dec 29, 2010

terminateEditOnFocusLost making problems for table in JDesktopPane–workaround for Java 6

 

There is a minor bug affecting JTable behaviour when used in connection with JInternalFrame and setting property “terminateEditOnFocusLost”. This bug is resolved in Java 7. For those who need the solution now, I have found a workaround.

The original bug description: (see http://bugs.sun.com/view_bug.do?bug_id=6505027 for details)

In 6.0, first I focused the TextField, and then I tried to focus on the second columns of the table which contain JComboBox. I was not able to perform this action on the second column.

In order to focus it first I have to focus the first columns and only the the table is focused then I can go to the second columns. This implies that the JComboBox can't get focus in the JTable when we using the command row:
Jtable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);.

It is a bug in JInternalFrame, fixed in Java 7. But for those who need a workaround and can't wait for Java 7, I have something. It turns out you can override JInternalFrame so that it uses fixed variant of the code (the same that's gonna be in Java 7). If you derive your internal frame from this class, the JTable problem will disappear. Here we go:

import java.awt.KeyboardFocusManager;
import java.awt.Window;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.JInternalFrame;
import javax.swing.SwingUtilities;
/*
 * Copies the fix for bug 6505027 from Java 7
 * http://hg.openjdk.java.net/jdk7/jdk7/jdk/rev/f727cac13697
 * Some support code (private) needed to be copied as well
 */
public class OKInternalFrame extends JInternalFrame {
    public OKInternalFrame() {
        addPropertyChangeListenerIfNecessary();
    }
    private java.awt.Component lastFocusOwner;
    private void setLastFocusOwner(java.awt.Component lastFocusOwner) {
        this.lastFocusOwner = lastFocusOwner;
    }
    private static boolean initializedFocusPropertyChangeListener = false;
    private static void addPropertyChangeListenerIfNecessary() {
        if (!initializedFocusPropertyChangeListener) {
            PropertyChangeListener focusListener =
                    new FocusPropertyChangeListener();
            initializedFocusPropertyChangeListener = true;
            KeyboardFocusManager.getCurrentKeyboardFocusManager().
                    addPropertyChangeListener(focusListener);
        }
    }
    private static class FocusPropertyChangeListener implements
            PropertyChangeListener {
        @Override
        public void propertyChange(PropertyChangeEvent e) {
            if (e.getPropertyName().equals("permanentFocusOwner")) {
                updateLastFocusOwner((java.awt.Component) e.getNewValue());
            }
        }
    }
    private static void updateLastFocusOwner(java.awt.Component component) {
        if (component != null) {
            java.awt.Component parent = component;
            while (parent != null && !(parent instanceof Window)) {
                if (parent instanceof OKInternalFrame) {
                    // Update lastFocusOwner for parent.
                    ((OKInternalFrame) parent).setLastFocusOwner(component);
                }
                parent = parent.getParent();
            }
        }
    }
    @Override
    public void restoreSubcomponentFocus() {
        if (isIcon()) {
            super.restoreSubcomponentFocus(); //delegated to super implementation, because it's correct there
        } else {
            java.awt.Component component = KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner();
            if ((component == null) || !SwingUtilities.isDescendingFrom(component, this)) {
                // FocusPropertyChangeListener will eventually update
                // lastFocusOwner. As focus requests are asynchronous
                // lastFocusOwner may be accessed before it has been correctly
                // updated. To avoid any problems, lastFocusOwner is immediately
                // set, assuming the request will succeed.
                setLastFocusOwner(getMostRecentFocusOwner());
                if (lastFocusOwner == null) {
                    // Make sure focus is restored somewhere, so that
                    // we don't leave a focused component in another frame while
                    // this frame is selected.
                    setLastFocusOwner(getContentPane());
                }
                if (!lastFocusOwner.hasFocus()) {
                    lastFocusOwner.requestFocus();
                }
            }
        }
    }
}

Dec 6, 2010

Xerces and offline validation with DTD

This was an issue that took me some time:

I need to validate a document using xerces parser. The document references a .xsd schema, which in turn references a DTD schema (http://www.w3.org/2001/XMLSchema.dtd). However, the document would not validate when there was no connection to Internet.

Using 

URL mySchemaURL = getClass().getResource("/com/mycompany/mySchema.xsd");
documentBuilderFactory.setAttribute("http://apache.org/xml/properties/schema/external-schemaLocation", "http://server.com/myschema " + mySchemaURL.toString());

I managed to make the parser read the XML schema from the JAR file. However this did not work for the DTD referenced from mySchema.xsd.

The error message was rather unhelpful:

org.xml.sax.SAXParseException: schema_reference.4: 
Failed to read schema document 'myschema.xsd', because 
1) could not find the document; 
2) the document could not be read; 
3) the root element of the document is not <xsd:schema>.

 


It turns out, the parser was trying to retrieve http://www.w3.org/2001/XMLSchema.dtd from the Internet. The solution with external-schemaLocation did not work for DTDs.


I then found an article (http://tynne.de/xerces-w3c) about caching DTDs, which led me to following solution:

class DTDResponseCache extends ResponseCache {
        Map<URI, String> savedDTD;
        /**
         * Original cache used for requests other than specified DTDs
         */
        ResponseCache originalCache;
        public DTDResponseCache(ResponseCache originalCache) {
            this.originalCache = originalCache;
            savedDTD = new HashMap<URI, String>();
            //Add your DTDs here
            savedDTD.put(URI.create("http://www.w3.org/2001/XMLSchema.dtd"),"/com/mycompany/dtds/XMLSchema.dtd");
            savedDTD.put(URI.create("http://www.w3.org/2001/datatypes.dtd"),"/com/mycompany/dtds/datatypes.dtd");
        }
        @Override
        public CacheResponse get(final URI uri, String rqstMethod, Map<String, List<String>> rqstHeaders) throws IOException {
            if (savedDTD.containsKey(uri)) {
                return new CacheResponse() {
                    @Override
                    public Map<String, List<String>> getHeaders() throws IOException {
                        Map<String, List<String>> headers = new HashMap<String, List<String>>();
                        return headers;
                    }
                    @Override
                    public InputStream getBody() throws IOException {
                        return getClass().getResourceAsStream(savedDTD.get(uri));
                    }
                };
            } else {
                if(originalCache != null){
                    return originalCache.get(uri, rqstMethod, rqstHeaders);
                } else {
                    return null;
                }
            }
        }
        @Override
        public CacheRequest put(URI uri, URLConnection conn) throws IOException {
            if (originalCache != null) {
                return originalCache.put(uri, conn);
            } else {
                return null;
            }
        }
    }


Now you only save the DTDs somewhere accesible to your classloader and then on application startup:


 

ResponseCache.setDefault(new DTDResponseCache(ResponseCache.getDefault()));
 

Which will preserve your previous caching settings (if any).