Blog Mateusza Chodyły czyli interdyscyplinarnego .NETowca 8-)

Wizualizacja drzewa dokumentu XML (Java)

17. maja 2010 19:18

Czas zobaczyć na co stać głównego konkurenta .NET Framework. Tym razem będę korzystał tylko ze standardowych kontrolek (JTree) i bibliotek.

Jedyną zmianą, jaką musiałem wprowadzić w XML Schema jest usunięcie ^ i $ z wyrażenia regularnego (widocznie są one dodawane domyślnie, co jest w sumie bezpieczniejszym podejściem - oznacza to, że domyślnie CAŁY ciąg musi spełniać podane wyrażenie). Odpowiednikiem .NET-owego XML Schema Definition tool jest JAXB. Wygenerowanie klas następuje po podaniu polecenia:
xjc -d "<dir>\src" -p schemaclasses "<dir>\src\xmlfiles"

Dostajemy łącznie 7 plików. Pokażę tylko trzy:

// *** AttachmentType.java ***

//
// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, vhudson-jaxb-ri-2.2-147 
// See <a href="http://java.sun.com/xml/jaxb">http://java.sun.com/xml/jaxb</a> 
// Any modifications to this file will be lost upon recompilation of the source schema. 
// Generated on: 2010.05.03 at 08:14:32 PM GMT 
//
package schemaclasses;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

/**
 * <p>Java class for attachmentType complex type.
 * 
 * <p>The following schema fragment specifies the expected content contained within this class.
 * 
 * <pre>
 * &lt;complexType name="attachmentType">
 *   &lt;complexContent>
 *     &lt;restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
 *       &lt;group ref="{urn:mails-schema}attachmentContent"/>
 *       &lt;attribute name="name" use="required" type="{http://www.w3.org/2001/XMLSchema}string" />
 *     &lt;/restriction>
 *   &lt;/complexContent>
 * &lt;/complexType>
 * </pre>
 * 
 * 
 */
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "attachmentType", propOrder =
{
  "mimetype",
  "content"
})
public class AttachmentType
{
  @XmlElement(required = true)
  protected AttachmentType.Mimetype mimetype;
  @XmlElement(required = true)
  protected String content;
  @XmlAttribute(name = "name", required = true)
  protected String name;

  public AttachmentType.Mimetype getMimetype()
  {
    return mimetype;
  }

  public void setMimetype(AttachmentType.Mimetype value)
  {
    this.mimetype = value;
  }

  public String getContent()
  {
    return content;
  }

  public void setContent(String value)
  {
    this.content = value;
  }

  public String getName()
  {
    return name;
  }

  public void setName(String value)
  {
    this.name = value;
  }

  /**
   * <p>Java class for anonymous complex type.
   *
   * <p>The following schema fragment specifies the expected content contained within this class.
   *
   * <pre>
   * &lt;complexType>
   *   &lt;complexContent>
   *     &lt;restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
   *       &lt;attGroup ref="{urn:mails-schema}mimeTypeAttributes"/>
   *     &lt;/restriction>
   *   &lt;/complexContent>
   * &lt;/complexType>
   * </pre>
   *
   *
   */
  @XmlAccessorType(XmlAccessType.FIELD)
  @XmlType(name = "")
  public static class Mimetype
  {
    @XmlAttribute(name = "type", required = true)
    protected MimeTopLevelType type;
    @XmlAttribute(name = "subtype", required = true)
    protected String subtype;

    public MimeTopLevelType getType()
    {
      return type;
    }

    public void setType(MimeTopLevelType value)
    {
      this.type = value;
    }

    public String getSubtype()
    {
      return subtype;
    }

    public void setSubtype(String value)
    {
      this.subtype = value;
    }
  }
}

// *** ObjectFactory.java ***

package schemaclasses;

import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.XmlElementDecl;
import javax.xml.bind.annotation.XmlRegistry;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;

@XmlRegistry
public class ObjectFactory
{
  private final static QName _Mails_QNAME = new QName("urn:mails-schema", "mails");
  private final static QName _Date_QNAME = new QName("urn:mails-schema", "date");

  public ObjectFactory()
  {
  }

  public MailsType createMailsType()
  {
    return new MailsType();
  }

  public AttachmentType.Mimetype createAttachmentTypeMimetype()
  {
    return new AttachmentType.Mimetype();
  }

  public MailType createMailType()
  {
    return new MailType();
  }

  public AttachmentType createAttachmentType()
  {
    return new AttachmentType();
  }

  public EnvelopeType createEnvelopeType()
  {
    return new EnvelopeType();
  }

  @XmlElementDecl(namespace = "urn:mails-schema", name = "mails")
  public JAXBElement<MailsType> createMails(MailsType value)
  {
    return new JAXBElement<MailsType>(_Mails_QNAME, MailsType.class, null, value);
  }

  @XmlElementDecl(namespace = "urn:mails-schema", name = "date")
  public JAXBElement<XMLGregorianCalendar> createDate(XMLGregorianCalendar value)
  {
    return new JAXBElement<XMLGregorianCalendar>(_Date_QNAME, XMLGregorianCalendar.class, null, value);
  }
}

// *** package-info.java ***

@javax.xml.bind.annotation.XmlSchema(namespace = "urn:mails-schema", elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED)
package schemaclasses;

// *** pomijam: ***
// EnvelopeType, MailType, MailsType i MimeTopLevelType

Chciałbym pokazać dwa podejścia do wyświetlania drzewa:

  • wykorzystanie DefaultTreeModel (podobne do wersji napisanej w .NET Framework),
  • stworzenie własnej implementacji TreeModel pod nazwą XMLTreeModel (wykorzystanie architektury MVC-podobnej). Napisałem MVC-podobnej, ponieważ istnieje ścisłe powiązanie pomiędzy widokiem i kontrolerem (model reprezentuje dane, widok jest ich wizualną reprezentacją, a kontroler przyjmuje wejście na widoku od użytkownika i tłumaczy je na zmiany w modelu - dlatego bardzo ciężko było napisać ogólny kontroler, który “nie znał” specyfiki widoku, więc zrezygnowano z niego w Swingu).

Zacznę od ciekawszej wersji, czyli własnej implementacji TreeModel. Model przedstawia się następująco:

package app;

import java.util.Vector;

import javax.swing.tree.*; // TreeModel, TreePath
import javax.swing.event.*; // TreeModelEvent, TreeModelListener

import org.w3c.dom.*; // Document, Element, Node, NodeList

public class XMLTreeModel implements TreeModel
{
  private Document document;
  Vector<TreeModelListener> listeners = new Vector<TreeModelListener>();

  public Document getDocument()
  {
    return document;
  }

  public void setDocument(Document document)
  {
    this.document = document;
    TreeModelEvent evt = new TreeModelEvent(this, new TreePath(getRoot()));
    for (TreeModelListener listener : listeners)
    {
      listener.treeStructureChanged(evt);
    }
  }

  public void addTreeModelListener(TreeModelListener listener)
  {
    if (!listeners.contains(listener))
    {
      listeners.add(listener);
    }
  }

  public void removeTreeModelListener(TreeModelListener listener)
  {
    listeners.remove(listener);
  }

  public Object getChild(Object parent, int index)
  {
    if (parent instanceof XMLTreeNode)
    {
      Vector<Element> elements = getChildElements(((XMLTreeNode) parent).getElement());
      return new XMLTreeNode(elements.get(index));
    }
    else
    {
      return null;
    }
  }

  public int getChildCount(Object parent)
  {
    if (parent instanceof XMLTreeNode)
    {
      Vector<Element> elements = getChildElements(((XMLTreeNode) parent).getElement());
      return elements.size();
    }
    return 0;
  }

  public int getIndexOfChild(Object parent, Object child)
  {
    if (parent instanceof XMLTreeNode && child instanceof XMLTreeNode)
    {
      Element pElement = ((XMLTreeNode) parent).getElement();
      Element cElement = ((XMLTreeNode) child).getElement();
      if (cElement.getParentNode() != pElement)
      {
        return -1;
      }
      Vector<Element> elements = getChildElements(pElement);
      return elements.indexOf(cElement);
    }
    return -1;
  }

  public Object getRoot()
  {
    if (document == null)
    {
      return null;
    }
    Vector<Element> elements = getChildElements(document);
    if (elements.size() > 0)
    {
      return new XMLTreeNode(elements.get(0));
    }
    else
    {
      return null;
    }
  }

  public boolean isLeaf(Object node)
  {
    if (node instanceof XMLTreeNode)
    {
      Element element = ((XMLTreeNode) node).getElement();
      Vector<Element> elements = getChildElements(element);
      return elements.size() == 0;
    }
    else
    {
      return true;
    }
  }

  public void valueForPathChanged(TreePath path, Object newValue)
  {
    throw new UnsupportedOperationException();
  }

  private Vector<Element> getChildElements(Node node)
  {
    Vector<Element> elements = new Vector<Element>();
    NodeList list = node.getChildNodes();
    for (int i = 0; i < list.getLength(); i++)
    {
      if (list.item(i).getNodeType() == Node.ELEMENT_NODE)
      {
        elements.add((Element) list.item(i));
      }
    }
    return elements;
  }
}

Następnie mamy klasę reprezentującą węzeł:

package app;

import org.w3c.dom.*; // Element, NodeList, Text

public class XMLTreeNode
{
  Element element;

  public XMLTreeNode(Element element)
  {
    this.element = element;
  }

  public Element getElement()
  {
    return element;
  }

  @Override
  public String toString()
  {
    String attrs = "";
    for (int i = 0; i < element.getAttributes().getLength(); i++)
    {
      attrs += element.getAttributes().item(i).getNodeName() + "=" + element.getAttributes().item(i).getNodeValue();
      if(i < (element.getAttributes().getLength() - 1))
      {
        attrs += ", ";
      }
    }

    if(attrs.equals(""))
    {
      return element.getNodeName();
    }
    else
    {
      return element.getNodeName() + " (" + attrs + ")";
    }
  }

  public String getContent()
  {
    NodeList list = element.getChildNodes();
    for (int i = 0; i < list.getLength(); i++)
    {
      if (list.item(i) instanceof Text)
      {
        return ((Text) list.item(i)).getTextContent();
      }
    }
    return "";
  }
}

I sam widok:

package app;

import java.awt.*; //BorderLayout, Dimension

import javax.swing.*; // JPanel, JScrollPane, JTextField, JTree
import javax.swing.event.*; // TreeSelectionEvent, TreeSelectionListener

import org.w3c.dom.Document;

public class XMLTreePanel extends JPanel
{
  private JTree tree;
  private XMLTreeModel model;

  public XMLTreePanel()
  {
    setLayout(new BorderLayout());

    model = new XMLTreeModel();
    tree = new JTree();
    tree.setModel(model);
    tree.setShowsRootHandles(true);
    tree.setEditable(false);

    JScrollPane pane = new JScrollPane(tree);
    pane.setPreferredSize(new Dimension(300, 400));

    add(pane, "Center");

    final JTextField text = new JTextField();
    text.setEditable(false);
    add(text, "South");

    tree.addTreeSelectionListener(new TreeSelectionListener()
    {
      public void valueChanged(TreeSelectionEvent e)
      {
        Object xtn = e.getPath().getLastPathComponent();
        if (xtn instanceof XMLTreeNode)
        {
          text.setText(((XMLTreeNode) xtn).getContent());
        }
      }
    });

  }

  public void setDocument(Document document)
  {
    model.setDocument(document);
  }

  public Document getDocument()
  {
    return model.getDocument();
  }
}

Teraz wykorzystam komponent i pokażę, jak skorzystać z domyślnego modelu (DefaultTreeModel):

package app;

import java.io.*;
import java.util.*;

import javax.swing.*; // JTree, JScrollPane, UIManager
import javax.swing.tree.*; // DefaultTreeModel, DefaultMutableTreeNode, TreeSelectionModel
import javax.swing.event.*; // TreeSelectionEvent, TreeSelectionListener
import javax.swing.filechooser.*; // FileFilter, FileNameExtensionFilter

import javax.xml.*;
import javax.xml.bind.*;
import javax.xml.validation.*;
import javax.xml.parsers.*; // DocumentBuilder, DocumentBuilderFactory
import org.w3c.dom.Document;

import schemaclasses.*;

public class XMLTreeViewerApp extends javax.swing.JFrame implements TreeSelectionListener
{
  private final JFileChooser fcDir = new JFileChooser();
  private final JFileChooser fcSchema = new JFileChooser();
  private File xmlFilesDir = null,  xsdFile = null;
  private FilenameFilter xmlFilter;
  private XMLTreePanel customTree;
  private JTree defaultTree;

  public static void main(String args[])
  {
    java.awt.EventQueue.invokeLater(new Runnable()
    {
      public void run()
      {
        try
        {
          UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        }
        catch (Exception e)
        {
        }

        new XMLTreeViewerApp().setVisible(true);
      }
    });
  }

  // <editor-fold desc="Inicjalizacja kontrolek">
  public XMLTreeViewerApp()
  {
    initComponents();

    xmlFilter = new FilenameFilter()
    {
      public boolean accept(File dir, String name)
      {
        return name.endsWith(".xml");
      }
    };
    fcDir.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);

    fcSchema.setFileSelectionMode(JFileChooser.FILES_ONLY);
    fcSchema.setFileFilter(new FileNameExtensionFilter("Pliki *.xsd", "xsd"));

    try
    {
      xmlFilesDir = new File(new File(".").getCanonicalPath() + "\\src\\xmlfiles");
      xsdFile = new File(new File(".").getCanonicalPath() + "\\src\\xmlfiles\\mails.xsd");

      if (xmlFilesDir.exists())
      {
        fcDir.setCurrentDirectory(xmlFilesDir);
        tbActiveDir.setText("Aktywny: " + xmlFilesDir.getAbsolutePath());
        fillComboFiles();
      }
      else
      {
        xmlFilesDir = null;
        tbActiveDir.setText("Aktywny: <nie wybrano>");
      }

      if (xsdFile.exists())
      {
        fcSchema.setCurrentDirectory(xsdFile);
        tbActiveSchema.setText("Aktywny: " + xsdFile.getCanonicalPath());
      }
      else
      {
        xsdFile = null;
        tbActiveSchema.setText("Aktywny: <nie wybrano>");
      }
    }
    catch (Exception ex)
    {
      JOptionPane.showMessageDialog(null, ex.getMessage(), "Błąd", JOptionPane.ERROR_MESSAGE);
    }

    customTree = new XMLTreePanel();
    customTree.setBounds(0, 0, pnlTreeUsingXmlTreeModel.getWidth(), pnlTreeUsingXmlTreeModel.getHeight());
    pnlTreeUsingXmlTreeModel.add(customTree);

    // z wykorzystaniem domyślnego modelu
    defaultTree = new JTree(new DefaultMutableTreeNode(new nodeType("mails", null, null)));
    defaultTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
    defaultTree.addTreeSelectionListener(this);
    defaultTree.setBounds(0, 0, pnlTreeUsingDefaultMutableTree.getWidth(), pnlTreeUsingDefaultMutableTree.getHeight());
    JScrollPane scrollPaneDefaultTree = new JScrollPane(defaultTree);
    scrollPaneDefaultTree.setBounds(0, 0, pnlTreeUsingDefaultMutableTree.getWidth(), pnlTreeUsingDefaultMutableTree.getHeight());
    pnlTreeUsingDefaultMutableTree.add(scrollPaneDefaultTree);
    pack();
  }

  private void fillComboFiles()
  {
    if (xmlFilesDir != null)
    {
      String[] fileNames = xmlFilesDir.list(xmlFilter);
      if (fileNames != null)
      {
        cmbFiles.removeAllItems();
        for (int i = 0; i < fileNames.length; i++)
        {
          cmbFiles.addItem(fileNames[i]);
        }
      }
    }
  }// </editor-fold>

  // <editor-fold desc="Obsługa DefaultTreeModel">
  private class nodeType
  {
    private String nodeTag,  nodeContent;
    private Map<String, String> nodeAttributes;

    public nodeType(String nodeTag, Map<String, String> nodeAttributes, String nodeContent)
    {
      this.nodeTag = nodeTag;
      this.nodeAttributes = nodeAttributes;
      this.nodeContent = nodeContent;
    }

    public String GetNodeContent()
    {
      return nodeContent;
    }

    @Override
    public String toString()
    {
      String attrConcat = "";

      if (nodeAttributes != null)
      {
        String[] keys = nodeAttributes.keySet().toArray(new String[0]);
        for (int i = 0; i < keys.length; i++)
        {
          attrConcat += keys[i] + "=" + nodeAttributes.get(keys[i]);

          if (i < (keys.length - 1))
          {
            attrConcat += ", ";
          }
        }
      }

      if (attrConcat.equals(""))
      {
        return nodeTag;
      }
      else
      {
        return nodeTag + " (" + attrConcat + ")";
      }
    }
  }

  public void valueChanged(TreeSelectionEvent e)
  {
    DefaultMutableTreeNode node = (DefaultMutableTreeNode) defaultTree.getLastSelectedPathComponent();

    if (node != null)
    {
      nodeType nodeInfo = (nodeType) node.getUserObject();
      tbNodeContent.setText(nodeInfo.GetNodeContent());
    }
  }// </editor-fold>

  private void btnValidateShowTreeActionPerformed(java.awt.event.ActionEvent evt)                                                    
  {                                                        
    if (cmbFiles.getSelectedItem() == null || xsdFile == null)
    {
      JOptionPane.showMessageDialog(null, "Nie wybrano pliku XML lub XSD", "Błąd", JOptionPane.ERROR_MESSAGE);
      return;
    }

    Schema mySchema = null;
    SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
    FileInputStream inStream = null;

    try
    {
      // walidacja:
      mySchema = sf.newSchema(xsdFile);

      JAXBContext jc = JAXBContext.newInstance("schemaclasses");
      Unmarshaller u = jc.createUnmarshaller();
      u.setSchema(mySchema);

      // dla DefaultMutableTree:
      inStream = new FileInputStream(xmlFilesDir + "\\" + cmbFiles.getSelectedItem().toString());
      JAXBElement<MailsType> root = (JAXBElement<MailsType>) u.unmarshal(inStream);

      DefaultTreeModel defTreeModel = (DefaultTreeModel)defaultTree.getModel();
      DefaultMutableTreeNode defMutableTreeNodeRoot = new DefaultMutableTreeNode(new nodeType("mails", null, null));
      defTreeModel.setRoot(defMutableTreeNodeRoot);

      List<MailType> mailsType = root.getValue().getMail();
      for (MailType mt : mailsType)
      {
        Map<String, String> attrs = new HashMap<String, String>();
        attrs.put("ID", mt.getID().toString());
        DefaultMutableTreeNode mail = new DefaultMutableTreeNode(new nodeType("mail", attrs, null));

        DefaultMutableTreeNode envelope = new DefaultMutableTreeNode(new nodeType("envelope", null, null));
        envelope.add(new DefaultMutableTreeNode(new nodeType("from", null, mt.getEnvelope().getFrom())));
        envelope.add(new DefaultMutableTreeNode(new nodeType("to", null, mt.getEnvelope().getTo())));
        envelope.add(new DefaultMutableTreeNode(new nodeType("date", null, mt.getEnvelope().getDate().toString())));
        envelope.add(new DefaultMutableTreeNode(new nodeType("subject", null, mt.getEnvelope().getSubject())));
        mail.add(envelope);

        mail.add(new DefaultMutableTreeNode(new nodeType("body", null, mt.getBody())));

        List<AttachmentType> attachmentsType = mt.getAttachment();
        for (AttachmentType at : attachmentsType)
        {
          attrs = new HashMap<String, String>();
          attrs.put("name", at.getName());
          DefaultMutableTreeNode attachment = new DefaultMutableTreeNode(new nodeType("attachment", attrs, null));

          attrs = new HashMap<String, String>();
          attrs.put("type", at.getMimetype().getType().value());
          attrs.put("subtype", at.getMimetype().getSubtype());
          attachment.add(new DefaultMutableTreeNode(new nodeType("mimetype", attrs, null)));
          attachment.add(new DefaultMutableTreeNode(new nodeType("content", null, at.getContent())));

          mail.add(attachment);
        }
        defMutableTreeNodeRoot.add(mail);
      }
      defTreeModel.reload();

      // dla XmlTreeModel:
      DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
      DocumentBuilder builder = dbFactory.newDocumentBuilder();
      Document document = builder.parse(new File(xmlFilesDir + "\\" + cmbFiles.getSelectedItem().toString()));
      document.normalize();
      customTree.setDocument(document);
    }
    catch (UnmarshalException ex)
    {
      JOptionPane.showMessageDialog(null, ex.getLinkedException().getMessage(), "Błąd parsowania", JOptionPane.ERROR_MESSAGE); // SAXParseException
    }
    catch (Exception ex)
    {
      JOptionPane.showMessageDialog(null, ex.getMessage(), "Błąd", JOptionPane.ERROR_MESSAGE);
    }
    finally
    {
      if (inStream != null)
      {
        try
        {
          inStream.close();
        }
        catch (IOException ex)
        {
        }
      }
    }
  }                                                   

  private void btnChooseDirActionPerformed(java.awt.event.ActionEvent evt)                                             
  {                                                 
    if (fcDir.showOpenDialog(this) == JFileChooser.APPROVE_OPTION)
    {
      xmlFilesDir = fcDir.getSelectedFile();
      tbActiveDir.setText("Aktywny: " + xmlFilesDir.getAbsolutePath());
      fillComboFiles();
    }
  }                                            

  private void btnChooseSchemaFileActionPerformed(java.awt.event.ActionEvent evt)                                                    
  {                                                        
    if (fcSchema.showOpenDialog(this) == JFileChooser.APPROVE_OPTION)
    {
      xsdFile = fcSchema.getSelectedFile();
      tbActiveSchema.setText("Aktywny: " + xsdFile.getAbsolutePath());
    }
  }                                                   
  
  @SuppressWarnings("unchecked")
  // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
  private void initComponents() 
  {
    // ... pomijam ...
  }// </editor-fold> 

  // Variables declaration - do not modify                     
  private javax.swing.JButton btnChooseDir;
  private javax.swing.JButton btnChooseSchemaFile;
  private javax.swing.JButton btnValidateShowTree;
  private javax.swing.JComboBox cmbFiles;
  private javax.swing.JLabel lblChooseDir;
  private javax.swing.JLabel lblChooseFile;
  private javax.swing.JLabel lblChooseSchemaFile;
  private javax.swing.JLabel lblTreeUsingDefaultMutableTree;
  private javax.swing.JLabel lblTreeUsingXmlTreeModel;
  private javax.swing.JPanel pnlParameters;
  private javax.swing.JPanel pnlTreeUsingDefaultMutableTree;
  private javax.swing.JPanel pnlTreeUsingXmlTreeModel;
  private javax.swing.JPanel pnlXmlTree;
  private javax.swing.JTextField tbActiveDir;
  private javax.swing.JTextField tbActiveSchema;
  private javax.swing.JTextField tbNodeContent;
  // End of variables declaration                   
}

Na koniec zrzut ekranu pokazujący aplikację w akcji:

xmlTreeViewer

Tagi: ,

Studia

Dodaj komentarz





  • Komentarz
  • Podgląd
Loading



Zmodyfikowany BlogEngine.NET (Bazowa szata graficzna: Mads Kristensen/Zdjęcia panoramiczne)
Hosting dzięki uprzejmości PCSS/Centrum Innowacji Microsoft
(c) Mateusz Chodyła (Panel logowania)