Initial version of a simple Navigator view for XML files of many flavors.
authorjglick@netbeans.org
Fri, 05 Aug 2005 12:25:41 -0400
changeset 17628fe510bc95984
parent 17627 0499f2073439
child 17629 fbb61f1eb972
Initial version of a simple Navigator view for XML files of many flavors.
xmlnavigation/src/org/netbeans/modules/xmlnavigation/XMLNavigatorPanel.java
xmlnavigation/test/unit/src/org/netbeans/modules/xmlnavigation/XMLNavigatorPanelTest.java
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/xmlnavigation/src/org/netbeans/modules/xmlnavigation/XMLNavigatorPanel.java	Fri Aug 05 12:25:41 2005 -0400
     1.3 @@ -0,0 +1,343 @@
     1.4 +/*
     1.5 + *                 Sun Public License Notice
     1.6 + *
     1.7 + * The contents of this file are subject to the Sun Public License
     1.8 + * Version 1.0 (the "License"). You may not use this file except in
     1.9 + * compliance with the License. A copy of the License is available at
    1.10 + * http://www.sun.com/
    1.11 + *
    1.12 + * The Original Code is NetBeans. The Initial Developer of the Original
    1.13 + * Code is Sun Microsystems, Inc. Portions Copyright 1997-2005 Sun
    1.14 + * Microsystems, Inc. All Rights Reserved.
    1.15 + */
    1.16 +
    1.17 +package org.netbeans.modules.xmlnavigation;
    1.18 +
    1.19 +import java.awt.Component;
    1.20 +import java.awt.EventQueue;
    1.21 +import java.awt.Toolkit;
    1.22 +import java.awt.event.ActionEvent;
    1.23 +import java.awt.event.KeyEvent;
    1.24 +import java.awt.event.MouseAdapter;
    1.25 +import java.awt.event.MouseEvent;
    1.26 +import java.io.IOException;
    1.27 +import java.io.StringReader;
    1.28 +import java.util.ArrayList;
    1.29 +import java.util.Arrays;
    1.30 +import java.util.Collection;
    1.31 +import java.util.List;
    1.32 +import java.util.Locale;
    1.33 +import javax.swing.AbstractAction;
    1.34 +import javax.swing.DefaultListCellRenderer;
    1.35 +import javax.swing.DefaultListModel;
    1.36 +import javax.swing.DefaultListSelectionModel;
    1.37 +import javax.swing.JComponent;
    1.38 +import javax.swing.JList;
    1.39 +import javax.swing.JScrollPane;
    1.40 +import javax.swing.KeyStroke;
    1.41 +import javax.swing.ListSelectionModel;
    1.42 +import javax.xml.parsers.ParserConfigurationException;
    1.43 +import javax.xml.parsers.SAXParser;
    1.44 +import javax.xml.parsers.SAXParserFactory;
    1.45 +import org.netbeans.api.xml.services.UserCatalog;
    1.46 +import org.netbeans.spi.navigator.NavigatorPanel;
    1.47 +import org.netbeans.spi.xml.cookies.DataObjectAdapters;
    1.48 +import org.openide.awt.MouseUtils;
    1.49 +import org.openide.cookies.LineCookie;
    1.50 +import org.openide.loaders.DataObject;
    1.51 +import org.openide.text.Line;
    1.52 +import org.openide.util.Lookup;
    1.53 +import org.openide.util.LookupEvent;
    1.54 +import org.openide.util.LookupListener;
    1.55 +import org.openide.util.RequestProcessor;
    1.56 +import org.xml.sax.Attributes;
    1.57 +import org.xml.sax.InputSource;
    1.58 +import org.xml.sax.Locator;
    1.59 +import org.xml.sax.SAXException;
    1.60 +import org.xml.sax.helpers.DefaultHandler;
    1.61 +
    1.62 +/**
    1.63 + * Displays an outline of an XML document.
    1.64 + * @author Jesse Glick
    1.65 + */
    1.66 +public final class XMLNavigatorPanel implements NavigatorPanel {
    1.67 +    
    1.68 +    private Lookup.Result selection;
    1.69 +    private final LookupListener selectionListener = new LookupListener() {
    1.70 +        public void resultChanged(LookupEvent ev) {
    1.71 +            display(selection.allInstances());
    1.72 +        }
    1.73 +    };
    1.74 +    private JComponent panel;
    1.75 +    private final DefaultListModel/*<Item>*/ listModel = new DefaultListModel();
    1.76 +    private ListSelectionModel/*<Item>*/ listSelectionModel;
    1.77 +    
    1.78 +    /**
    1.79 +     * Default constructor for layer.
    1.80 +     */
    1.81 +    public XMLNavigatorPanel() {}
    1.82 +    
    1.83 +    public String getDisplayName() {
    1.84 +        return "XML Outline"; // XXX I18N
    1.85 +    }
    1.86 +    
    1.87 +    public String getDisplayHint() {
    1.88 +        return "Displays an outline of interesting XML elements."; // XXX I18N
    1.89 +    }
    1.90 +    
    1.91 +    public JComponent getComponent() {
    1.92 +        if (panel == null) {
    1.93 +            listSelectionModel = new DefaultListSelectionModel();
    1.94 +            final JList/*<Item>*/ view = new JList(listModel);
    1.95 +            view.setSelectionModel(listSelectionModel);
    1.96 +            view.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    1.97 +            view.addMouseListener(new MouseAdapter() {
    1.98 +                public void mouseClicked(MouseEvent e) {
    1.99 +                    if (MouseUtils.isDoubleClick(e)) {
   1.100 +                        int index = view.locationToIndex(e.getPoint());
   1.101 +                        open(index);
   1.102 +                    }
   1.103 +                }
   1.104 +            });
   1.105 +            view.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "open"); // NOI18N
   1.106 +            view.getActionMap().put("open", new AbstractAction() { // NOI18N
   1.107 +                public void actionPerformed(ActionEvent e) {
   1.108 +                    open(listSelectionModel.getLeadSelectionIndex());
   1.109 +                }
   1.110 +            });
   1.111 +            view.setCellRenderer(new ItemCellRenderer());
   1.112 +            panel = new JScrollPane(view) {
   1.113 +                public boolean requestFocusInWindow() {
   1.114 +                    boolean b = view.requestFocusInWindow();
   1.115 +                    if (!listModel.isEmpty() && listSelectionModel.isSelectionEmpty()) {
   1.116 +                        listSelectionModel.setSelectionInterval(0, 0);
   1.117 +                    }
   1.118 +                    return b;
   1.119 +                }
   1.120 +            };
   1.121 +        }
   1.122 +        return panel;
   1.123 +    }
   1.124 +    
   1.125 +    public void panelActivated(Lookup context) {
   1.126 +        selection = context.lookup(new Lookup.Template(DataObject.class));
   1.127 +        selection.addLookupListener(selectionListener);
   1.128 +        selectionListener.resultChanged(null);
   1.129 +        // XXX should also listen to changes in active Document and reparse after a short delay...
   1.130 +        // workaround: just switch tabs and back
   1.131 +    }
   1.132 +    
   1.133 +    public void panelDeactivated() {
   1.134 +        selection.removeLookupListener(selectionListener);
   1.135 +        selection = null;
   1.136 +    }
   1.137 +    
   1.138 +    public Lookup getLookup() {
   1.139 +        return null;
   1.140 +    }
   1.141 +    
   1.142 +    private void display(Collection/*<DataObject>*/ selectedFiles) {
   1.143 +        listModel.clear();
   1.144 +        // Show list of targets for selected file:
   1.145 +        if (selectedFiles.size() == 1) {
   1.146 +            final DataObject d = (DataObject) selectedFiles.iterator().next();
   1.147 +            final InputSource src = DataObjectAdapters.inputSource(d);
   1.148 +            // Parse asynch, since it can take a second or two for a big file.
   1.149 +            RequestProcessor.getDefault().post(new Runnable() {
   1.150 +                public void run() {
   1.151 +                    try {
   1.152 +                        final Item[] items = parse(src, d);
   1.153 +                        EventQueue.invokeLater(new Runnable() {
   1.154 +                            public void run() {
   1.155 +                                for (int i = 0; i < items.length; i++) {
   1.156 +                                    listModel.addElement(items[i]);
   1.157 +                                }
   1.158 +                            }
   1.159 +                        });
   1.160 +                    } catch (Exception e) { // IOException, SAXParseException
   1.161 +                        // ignore for now
   1.162 +                    }
   1.163 +                }
   1.164 +            });
   1.165 +        }
   1.166 +    }
   1.167 +    
   1.168 +    private void open(int listIndex) {
   1.169 +        if (listIndex < 0 || listIndex >= listModel.size()) {
   1.170 +            Toolkit.getDefaultToolkit().beep();
   1.171 +            return;
   1.172 +        }
   1.173 +        Item item = (Item) listModel.get(listIndex);
   1.174 +        item.open();
   1.175 +    }
   1.176 +
   1.177 +    /**
   1.178 +     * Names of elements which should be considered headers.
   1.179 +     */
   1.180 +    private static final List/*<String>*/ HEADERS = Arrays.asList(new String[] {
   1.181 +        "h1", // NOI18N
   1.182 +        "h2", // NOI18N
   1.183 +        "h3", // NOI18N
   1.184 +        "h4", // NOI18N
   1.185 +        "h5", // NOI18N
   1.186 +        "h6", // NOI18N
   1.187 +    });
   1.188 +    /**
   1.189 +     * Names of elements whose parent elements should be considered headers.
   1.190 +     */
   1.191 +    private static final List/*<String>*/ TITLES = Arrays.asList(new String[] {
   1.192 +        "title", // NOI18N
   1.193 +    });
   1.194 +    /**
   1.195 +     * Names of attributes which if on elements should be considered headers.
   1.196 +     */
   1.197 +    private static final List/*<String>*/ NAMES = Arrays.asList(new String[] {
   1.198 +        "name", // NOI18N
   1.199 +        "id", // NOI18N
   1.200 +        // For XSL:
   1.201 +        "match", // NOI18N
   1.202 +    });
   1.203 +    
   1.204 +    static Item[] parse(InputSource src, final DataObject d) throws IOException, SAXException, ParserConfigurationException {
   1.205 +        final List/*<Item>*/ items = new ArrayList();
   1.206 +        SAXParserFactory factory = SAXParserFactory.newInstance();
   1.207 +        SAXParser parser = factory.newSAXParser();
   1.208 +        class Handler extends DefaultHandler {
   1.209 +            private Locator locator;
   1.210 +            private int line = -1;
   1.211 +            private String element = null;
   1.212 +            private StringBuffer text = null;
   1.213 +            public void setDocumentLocator(Locator l) {
   1.214 +                locator = l;
   1.215 +            }
   1.216 +            // XXX do not match <xsl:call-template name="..."/>!
   1.217 +            // Better style would perhaps be to have include/exclude lists
   1.218 +            // which would be pairs of NS-qualified element name plus attr name... TBD.
   1.219 +            public void startElement(String uri, String localname, String qname, Attributes attr) throws SAXException {
   1.220 +                text = null;
   1.221 +                if (HEADERS.contains(qname.toLowerCase(Locale.ENGLISH))) {
   1.222 +                    text = new StringBuffer();
   1.223 +                    line = locator.getLineNumber();
   1.224 +                    element = null;
   1.225 +                    //System.err.println("HEADERS match on " + qname + " at " + line);
   1.226 +                } else if (TITLES.contains(qname.toLowerCase(Locale.ENGLISH))) {
   1.227 +                    if (element != null) {
   1.228 +                        text = new StringBuffer();
   1.229 +                        //System.err.println("TITLES match on " + qname + " inside " + element + " at line " + line);
   1.230 +                    }
   1.231 +                } else {
   1.232 +                    line = locator.getLineNumber();
   1.233 +                    element = qname;
   1.234 +                    //System.err.println("plain element " + element + " at " + line);
   1.235 +                    for (int i = 0; i < attr.getLength(); i++) {
   1.236 +                        String name = attr.getQName(i);
   1.237 +                        if (NAMES.contains(name.toLowerCase(Locale.ENGLISH))) {
   1.238 +                            //System.err.println("NAMES match on " + name + " in " + element + " at " + line);
   1.239 +                            items.add(new Item(attr.getValue(i), element, name, line, d));
   1.240 +                            break;
   1.241 +                        }
   1.242 +                    }
   1.243 +                }
   1.244 +            }
   1.245 +            public void endElement(String uri, String localname, String qname) throws SAXException {
   1.246 +                if (text != null) {
   1.247 +                    //System.err.println("ending " + qname + " in " + element + " with " + text + " at " + line);
   1.248 +                    assert line != -1;
   1.249 +                    if (element == null) {
   1.250 +                        element = qname;
   1.251 +                    }
   1.252 +                    items.add(new Item(text.toString(), element, qname, line, d));
   1.253 +                    text = null;
   1.254 +                    element = null;
   1.255 +                    line = -1;
   1.256 +                }
   1.257 +            }
   1.258 +            public void characters(char[] ch, int start, int length) throws SAXException {
   1.259 +                if (text != null) {
   1.260 +                    text.append(ch, start, length);
   1.261 +                }
   1.262 +            }
   1.263 +            public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
   1.264 +                InputSource known;
   1.265 +                try {
   1.266 +                    known = UserCatalog.getDefault().getEntityResolver().resolveEntity(publicId, systemId);
   1.267 +                } catch (IOException e) {
   1.268 +                    throw new SAXException(e);
   1.269 +                }
   1.270 +                if (known != null) {
   1.271 +                    // In our IDE catalog, cool.
   1.272 +                    //System.err.println("known match on " + publicId + " / " + systemId + ": " + known.getSystemId());
   1.273 +                    return known;
   1.274 +                } else if (systemId.startsWith("http")) { // NOI18N
   1.275 +                    // Do not load any remote entities or DTDs, too slow.
   1.276 +                    //System.err.println("No known match for remote " + publicId + " / " + systemId);
   1.277 +                    return new InputSource(new StringReader(""));
   1.278 +                } else {
   1.279 +                    // Maybe a local file: URL or similar.
   1.280 +                    return null;
   1.281 +                }
   1.282 +            }
   1.283 +            /*
   1.284 +            public void skippedEntity(String name) throws SAXException {
   1.285 +                System.err.println("skipped: " + name);
   1.286 +            }
   1.287 +             */
   1.288 +        }
   1.289 +        parser.parse(src, new Handler());
   1.290 +        return (Item[]) items.toArray(new Item[items.size()]);
   1.291 +    }
   1.292 +    
   1.293 +    static final class Item {
   1.294 +        private final String label;
   1.295 +        private final String element;
   1.296 +        private final String header;
   1.297 +        private final int line;
   1.298 +        private final DataObject d;
   1.299 +        public Item(String label, String element, String header, int line, DataObject d) {
   1.300 +            this.label = label;
   1.301 +            this.element = element;
   1.302 +            this.header = header;
   1.303 +            this.line = line /* SAX is 1-based */ - 1;
   1.304 +            this.d = d;
   1.305 +        }
   1.306 +        public String getLabel() {
   1.307 +            return label;
   1.308 +        }
   1.309 +        public String getElement() {
   1.310 +            return element;
   1.311 +        }
   1.312 +        public String getHeader() {
   1.313 +            return header;
   1.314 +        }
   1.315 +        int getLine() {
   1.316 +            return line;
   1.317 +        }
   1.318 +        public void open() {
   1.319 +            LineCookie cookie = (LineCookie) d.getCookie(LineCookie.class);
   1.320 +            if (cookie == null) {
   1.321 +                Toolkit.getDefaultToolkit().beep();
   1.322 +                return;
   1.323 +            }
   1.324 +            Line l;
   1.325 +            try {
   1.326 +                l = cookie.getLineSet().getCurrent(line);
   1.327 +            } catch (IndexOutOfBoundsException ex) {
   1.328 +                Toolkit.getDefaultToolkit().beep();
   1.329 +                return;
   1.330 +            }
   1.331 +            l.show(Line.SHOW_TOFRONT);
   1.332 +        }
   1.333 +    }
   1.334 +    
   1.335 +    private static final class ItemCellRenderer extends DefaultListCellRenderer {
   1.336 +        
   1.337 +        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
   1.338 +            Item item = (Item) value;
   1.339 +            // XXX could also display element and/or header in a different color (use HTMLRenderer)
   1.340 +            String text = item.getLabel();
   1.341 +            return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus);
   1.342 +        }
   1.343 +        
   1.344 +    }
   1.345 +    
   1.346 +}
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/xmlnavigation/test/unit/src/org/netbeans/modules/xmlnavigation/XMLNavigatorPanelTest.java	Fri Aug 05 12:25:41 2005 -0400
     2.3 @@ -0,0 +1,106 @@
     2.4 +/*
     2.5 + *                 Sun Public License Notice
     2.6 + *
     2.7 + * The contents of this file are subject to the Sun Public License
     2.8 + * Version 1.0 (the "License"). You may not use this file except in
     2.9 + * compliance with the License. A copy of the License is available at
    2.10 + * http://www.sun.com/
    2.11 + *
    2.12 + * The Original Code is NetBeans. The Initial Developer of the Original
    2.13 + * Code is Sun Microsystems, Inc. Portions Copyright 1997-2005 Sun
    2.14 + * Microsystems, Inc. All Rights Reserved.
    2.15 + */
    2.16 +
    2.17 +package org.netbeans.modules.xmlnavigation;
    2.18 +
    2.19 +import java.io.StringReader;
    2.20 +import org.netbeans.junit.NbTestCase;
    2.21 +import org.netbeans.modules.xmlnavigation.XMLNavigatorPanel.Item;
    2.22 +import org.xml.sax.InputSource;
    2.23 +
    2.24 +/**
    2.25 + * Test functionality of {@link XMLNavigatorPanel}.
    2.26 + * @author Jesse Glick
    2.27 + */
    2.28 +public class XMLNavigatorPanelTest extends NbTestCase {
    2.29 +    
    2.30 +    public XMLNavigatorPanelTest(String name) {
    2.31 +        super(name);
    2.32 +    }
    2.33 +    
    2.34 +    public void testParse() throws Exception {
    2.35 +        assertEquals("correct parse of some XHTML sections",
    2.36 +            "1[h1/h1]Intro\n" +
    2.37 +            "6[h1/h1]Main Section\n" +
    2.38 +            "7[h2/h2]Subsection\n" +
    2.39 +            "10[h1/h1]Conclusion\n",
    2.40 +            itemsSummary(parse(
    2.41 +                "<body>\n" + // 0
    2.42 +                "<h1>Intro</h1>\n" + // 1
    2.43 +                "<p>\n" + // 2
    2.44 +                "Hello!\n" + // 3
    2.45 +                "</p>\n" + // 4
    2.46 +                "\n" + // 5
    2.47 +                "<h1>Main Section</h1>\n" + // 6
    2.48 +                "<h2>Subsection</h2>\n" + // 7
    2.49 +                "<p>More...</p>\n" + // 8
    2.50 +                "\n" + // 9
    2.51 +                "<h1>Conclusion</h1>\n" + // 10
    2.52 +                "\n" + // 11
    2.53 +                "</body>\n" // 12
    2.54 +                )));
    2.55 +        assertEquals("correct parse of some XHTML anchors",
    2.56 +            "1[a/name]here\n" +
    2.57 +            "6[a/id]there\n",
    2.58 +            itemsSummary(parse(
    2.59 +                "<body>\n" + // 0
    2.60 +                "<a name='here'>Stuff...</a>\n" + // 1
    2.61 +                "<p>\n" + // 2
    2.62 +                "Hello!\n" + // 3
    2.63 +                "</p>\n" + // 4
    2.64 +                "\n" + // 5
    2.65 +                "<a id='there'>More stuff...</a>\n" + // 6
    2.66 +                "</body>\n" // 7
    2.67 +                )));
    2.68 +        assertEquals("correct parse of some DocBook sections",
    2.69 +            "1[section/title]First\n" +
    2.70 +            "4[section/title]Second\n" +
    2.71 +            "6[section/title]Notes on Second\n",
    2.72 +            itemsSummary(parse(
    2.73 +                "<article>\n" + // 0
    2.74 +                " <section><title>First</title>\n" + // 1
    2.75 +                "  <para>Stuff...</para>\n" + // 2
    2.76 +                " </section>\n" + // 3
    2.77 +                " <section><title>Second</title>\n" + // 4
    2.78 +                "  <para>Stuff...</para>\n" + // 5
    2.79 +                "  <section><title>Notes on Second</title>\n" + // 6
    2.80 +                "   <para>Stuff...</para>\n" + // 7
    2.81 +                "  </section>\n" + // 8
    2.82 +                " </section>\n" + // 9
    2.83 +                "</article>\n" // 10
    2.84 +                )));
    2.85 +        // XXX should DocBook subsections be marked differently?
    2.86 +        // XXX should try to also parse e.g. <h3><a name="section">Title here...</a></h3>
    2.87 +        // as well as <section id="section"><title>Title here...</title>...</section>
    2.88 +    }
    2.89 +    
    2.90 +    private static Item[] parse(String xml) throws Exception {
    2.91 +        return XMLNavigatorPanel.parse(new InputSource(new StringReader(xml)), null);
    2.92 +    }
    2.93 +    
    2.94 +    private static String itemsSummary(Item[] items) {
    2.95 +        StringBuffer b = new StringBuffer();
    2.96 +        for (int i = 0; i < items.length; i++) {
    2.97 +            b.append(items[i].getLine());
    2.98 +            b.append('[');
    2.99 +            b.append(items[i].getElement());
   2.100 +            b.append('/');
   2.101 +            b.append(items[i].getHeader());
   2.102 +            b.append(']');
   2.103 +            b.append(items[i].getLabel());
   2.104 +            b.append('\n');
   2.105 +        }
   2.106 +        return b.toString();
   2.107 +    }
   2.108 +    
   2.109 +}