001package com.randomnoun.common.jna;
002
003/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
004 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
005 */
006
007import java.util.HashMap;
008import java.util.Map;
009
010import javax.xml.parsers.DocumentBuilder;
011import javax.xml.parsers.DocumentBuilderFactory;
012import javax.xml.parsers.ParserConfigurationException;
013import javax.xml.transform.TransformerException;
014
015import org.apache.log4j.Logger;
016import org.w3c.dom.Document;
017import org.w3c.dom.Element;
018
019import com.randomnoun.common.XmlUtil;
020import com.sun.jna.Native;
021import com.sun.jna.Pointer;
022import com.sun.jna.platform.win32.WinDef.DWORD;
023import com.sun.jna.platform.win32.WinDef.HWND;
024import com.sun.jna.platform.win32.WinUser;
025import com.sun.jna.platform.win32.WinUser.WNDENUMPROC;
026import com.sun.jna.win32.StdCallLibrary;
027import com.sun.jna.win32.W32APIOptions;
028
029/** A class to convert the Win32 windows tree into a DOM object
030 * 
031 * @see <a href="http://www.randomnoun.com/wp/2012/12/26/automating-windows-from-java-and-windowtreedom/">http://www.randomnoun.com/wp/2012/12/26/automating-windows-from-java-and-windowtreedom/</a>
032 * @author knoxg
033 */
034public class WindowTreeDom {
035
036        // the User32 functions we invoke from this class
037        public interface User32 extends StdCallLibrary {
038      User32 INSTANCE = (User32) Native.loadLibrary("user32", User32.class, 
039        W32APIOptions.DEFAULT_OPTIONS);
040      
041      public static final DWORD GW_OWNER = new DWORD(4);
042      boolean EnumWindows(WinUser.WNDENUMPROC lpEnumFunc, Pointer arg);
043      boolean EnumChildWindows(HWND hWnd, WNDENUMPROC lpEnumFunc, Pointer data);
044      int GetWindowText(HWND hWnd, char[] lpString, int nMaxCount);
045      int GetClassName(HWND hWnd, char[] lpClassName, int nMaxCount);
046      public HWND GetWindow(HWND hWnd, DWORD cmd);
047      HWND GetParent(HWND hWnd);
048    }
049        
050        /** JNA interface to USER32.DLL */
051        final static User32 lib = User32.INSTANCE;
052
053        /** Logger instance for this class */
054        static Logger logger = Logger.getLogger(WindowTreeDom.class);
055        
056        /** WindowTreeDom constructor.
057         * 
058         * @see #getDom()
059         */
060        public WindowTreeDom() {
061                
062        }
063        
064        /** This callback is invoked for each window found. It generates XML 
065         * {#link org.w3c.Element}s for each window, and attaches them to the supplied 
066         * {#link org.w3c.Document}.
067         * 
068         */
069        private static class WindowCallback implements WinUser.WNDENUMPROC {
070                Document doc;
071                Element documentElement;
072                Element topLevelWindow; 
073                Map<String, Element> hwndMap = new HashMap<String, Element>();
074                
075                /** Creates a new window callback
076                 * 
077                 * @param doc The XML document populated by this callback.
078                 * @param topLevelHWND If non-null, the windows being returned should all be
079                 *   child windows of this HWND (via EnumChildWindows), otherwise it is
080                 *   assumed toplevel windows are returned (via EnumWindows)
081                 * @param topLevelWindow If non-null, the document Element within <tt>doc</tt>
082                 *   which will contain new child elements.
083                 */
084                public WindowCallback(Document doc, HWND topLevelHWND, Element topLevelWindow) {
085                        this.doc = doc;
086                        this.topLevelWindow = topLevelWindow;
087                        if (topLevelWindow != null) {
088                                hwndMap.put(topLevelHWND.getPointer().toString(), topLevelWindow);
089                        }
090                        documentElement = doc.getDocumentElement();
091                }
092                
093                public boolean callback(HWND hWnd, Pointer data) {
094                        
095                        char[] buffer = new char[512];
096                        User32.INSTANCE.GetWindowText(hWnd, buffer, 512);
097                        
098                        char[] buffer2 = new char[1026];
099                        int classLen = User32.INSTANCE.GetClassName(hWnd, buffer2, 1026);
100                    
101                        String windowTitle = Native.toString(buffer);
102                        String className = Native.toString(buffer2);
103
104                        HWND parent = User32.INSTANCE.GetParent(hWnd);
105                        HWND owner = User32.INSTANCE.GetWindow(hWnd, User32.GW_OWNER);
106                        
107                    // check if this has already been created in the DOM
108                        Element el = hwndMap.get(hWnd.getPointer().toString());
109                        if (el==null) {
110                                el = doc.createElement("window");
111                        } else {
112                                el.removeAttribute("pwindow");
113                        }
114                        el.setAttribute("hwnd", hWnd.getPointer().toString());
115                        if (owner!=null) {
116                                el.setAttribute("owner", owner.getPointer().toString());
117                        }
118                        el.setAttribute("title", windowTitle);
119                        el.setAttribute("class", className);
120                        
121                        hwndMap.put(hWnd.getPointer().toString(), el);
122                        if (topLevelWindow==null) { 
123                                // this is a real top level element, so enumerate its children
124                                WindowCallback childDommer = new WindowCallback(doc, hWnd, el);
125                                // this code relies on being able to enum child windows whilst enumming toplevel windows
126                                lib.EnumChildWindows (hWnd, childDommer, new Pointer(0));
127                                try {
128                                        childDommer.checkForOrphanedWindows();
129                                } catch (TransformerException e) {
130                                        logger.error("Problem serialising orphaned windows to XML", e);
131                                }
132                        }
133                        
134                        if (parent==null) {
135                                documentElement.appendChild(el);
136                                if (topLevelWindow!=null) {
137                                        // have seen VMDragDetectWndClass'es here, presumably a vmware thing
138                                        // (note that this window won't be in the parent callback's hwndMap)
139                                        try {
140                                                logger.warn("Toplevel child window found: " + XmlUtil.getXmlString(el, true));
141                                        } catch (TransformerException e) {
142                                                logger.error("Toplevel child window found, problem serialising toplevel windows to XML", e);
143                                        }
144                                }
145                                
146                        } else {
147                                Element parentEl = hwndMap.get(parent.getPointer().toString());
148                                if (parentEl==null) {
149                                        // throw new IllegalStateException("Unknown parent window '" + parent.getPointer().toString() + "'");
150                                        // it appears that we can get IME child windows being returned 
151                                        // by EnumWindows, even though they're not top-level
152                                        parentEl = doc.createElement("window");
153                                        parentEl.setAttribute("pwindow", "true");
154                                        parentEl.setAttribute("hwnd", parent.getPointer().toString());
155                                        hwndMap.put(parent.getPointer().toString(), parentEl); 
156                                }
157                                parentEl.appendChild(el);
158                        }
159                        
160                        return true;
161                }
162                
163                /** Lists any window nodes that were generated via enumeration, whose 
164                 * parent nodes were not generated.
165                 * 
166                 * @throws TransformerException
167                 */
168                public void checkForOrphanedWindows() throws TransformerException {
169                        for (Element e : hwndMap.values()) {
170                                if (!e.getAttribute("pwindow").equals("")) {
171                                        // the desktop window isn't in the enumeration
172                                        logger.warn("Parent window found that was not in enumeration: " + XmlUtil.getXmlString(e, true));
173                                        // throw new IllegalStateException("Window found without parent window");
174                                }
175                        }
176                }
177
178        }
179        
180        /** Generate an XML document from the Win32 window tree */
181        public Document getDom() throws ParserConfigurationException, TransformerException {
182                DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
183                DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
184                Document doc = docBuilder.newDocument();
185                Element topElement = doc.createElement("windows");
186                doc.appendChild(topElement);
187                
188                WindowCallback dommer = new WindowCallback(doc, null, null);
189                lib.EnumWindows (dommer, new Pointer(0));
190                dommer.checkForOrphanedWindows();
191                
192                return doc;
193        }
194        
195        /** Return the hwnd of an element, as a pointer represented as a long 
196         * 
197         * @param windowEl a window element returned from getDom()
198         * 
199         * @return the hwnd of the element.
200         */
201        public HWND getHwnd(Element windowEl) {
202        String hwndString = windowEl.getAttribute("hwnd");
203        if (hwndString.startsWith("native@0x")) {
204                return new HWND(new Pointer(Long.parseLong(hwndString.substring(9), 16)));
205        } else {
206                throw new IllegalStateException("Could not determine HWND of window element: found '" + hwndString + "'");
207        }
208        }
209        
210}