Initial version of a simple Navigator view for XML files of many flavors.
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 +}