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>
* <complexType name="attachmentType">
* <complexContent>
* <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
* <group ref="{urn:mails-schema}attachmentContent"/>
* <attribute name="name" use="required" type="{http://www.w3.org/2001/XMLSchema}string" />
* </restriction>
* </complexContent>
* </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>
* <complexType>
* <complexContent>
* <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
* <attGroup ref="{urn:mails-schema}mimeTypeAttributes"/>
* </restriction>
* </complexContent>
* </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:
