|
El API JAXP |
- Mostrar un Árbol DOM
Mostrar un Árbol DOM
Para crear o manipular un DOM, nos ayudará tener una ídea de cómo están estructurados los nodos en un DOM. En esta sección del tutorial, expondremos la estructura interna de un DOM.
Mostrar los Nodos del Árbol
En la primera sección del tutorial DOM, usamos el método write de XmlDocument para la salida de los datos XML. La salida parecía igual que la entrada, lo que era bueno, pero el resultado no ayudaba a visualizar la estructura interna de un DOM.
Lo que necesitamos en este momento es una forma de exponer los nodos de un DOM para que podamos ver sus contenidos. Para hacer esto, convertiremos un DOM en un JTreeModel y mostraremos todo el DOM en un JTree. Nos va a llevar un poco de trabajo, pero el resultado final será una herramienta de diagnóstico que podremos usar en el futuro, así como todo lo que podremos aprender sobre la estructura de un DOM ahora.
Convertir DomEcho en una Aplicación GUI
Como el DOM es un árbol, y el componente JTree de Swing puede mostrar árboles, tiene sentido meter el DOM en un JTree, para poder verlo. El primer paso en este proceso es modificar el programa DomEcho para que se convierta en una aplicación GUI.
|
Nota:
El código explicado en ésta sección está en DomEcho02.java. |
Añadir las sentencias Import
Empezaremos eliminado la sentencia import de XmlDocument. No la necesitaremos más ya que no vamos a usar la operación write.
import java.io.File; import java.io.IOException; import com.sun.xml.tree.XmlDocument;
Luego, importaremos los componentes GUI que vamos a necesitar para configurar la aplicación y mostrar un JTree:
// GUI components and layouts import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTree;
Luego, incorporamos los componentes que necesitamos para configurar una vista dividida (JSplitPane) y para mostrar el texto de los subelementos (JEditorPane).
import javax.swing.JSplitPane; import javax.swing.JEditorPane;
Añadimos unas cuantas clases de soporte que vamos a necesitar para separar estas cosas de la tierra:
// GUI support classes import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Toolkit; import java.awt.event.WindowEvent; import java.awt.event.WindowAdapter;
Finalmente, importaremos algunas clases para hacer un borde divertido:
// For creating borders import javax.swing.border.EmptyBorder; import javax.swing.border.BevelBorder; import javax.swing.border.CompoundBorder;
(Estas son opcionales. Podemos saltárnoslas y el código que depende de ellas para hacer las cosas más sencillas).
Crear el Marco de Trabajo GUI
El siguiente paso es convertir la aplicación en una aplicación GUI. Para hacer esto, el método main creará un ejemplar de la clase principal, que convertiremos en un panel GUI. Empezaremos convirtiendo la clase en un panel GUI extendiendo la clase JPanel de Swing.
public class DomEcho02 extends JPanel
{
// Global value so it can be ref'd by the tree-adapter
static Document document;
...
Ya que estamos aquí, definimos unas pocas constantes que usaremos para controlar el tamaño de las ventanas:
public class DomEcho02 extends JPanel
{
// Global value so it can be ref'd by the tree-adapter
static Document document;
static final int windowHeight = 460;
static final int leftWidth = 300;
static final int rightWidth = 340;
static final int windowWidth = leftWidth + rightWidth;
Ahora, en el método main, eliminamos las líneas que escribían los datos XML en System.out, y en su lugar llamamos al método que crea el marco externo en el que situaremos el panel GUI:
public static void main (String argv [])
{
...
DocumentBuilderFactory factory ...
try {
DocumentBuilder builder = factory.newDocumentBuilder();
document = builder.parse( new File(argv[0]) );
XmlDocument xdoc = (XmlDocument) document;
xdoc.write (System.out);
makeFrame();
} catch (SAXParseException spe) {
...
Luego, necesitamos definir el propio método makeFrame. Contiene el método estándard para crear un frame, manejar de forma agradable las condiciones de salida, obtener un ejemplar del panel principal, dimensionarlo, localizarlo sobre la pantalla, y hacerlo visible:
...
} // main
public static void makeFrame()
{
// Set up a GUI framework
JFrame frame = new JFrame("DOM Echo");
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {System.exit(0);}
});
// Set up the tree, the views, and display it all
final DomEcho02 echoPanel = new DomEcho02();
frame.getContentPane().add("Center", echoPanel );
frame.pack();
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
int w = windowWidth + 10;
int h = windowHeight + 10;
frame.setLocation(screenSize.width/3 - w/2, screenSize.height/2 - h/2);
frame.setSize(w, h);
frame.setVisible(true);
} // makeFrame
Añadir los Compontes de Pantalla
Lo único que nos falta en el esfuerzo por convertir el programa en una aplicación GUI es crear el constructor de la clase y hacer que cree los contenidos del panel. Aquí está el constructor:
public class DomEcho02 extends JPanel
{
...
static final int windowWidth = leftWidth + rightWidth;
public DomEcho02()
{
} // Constructor
Aquí, hacemos uso de la clase border que importamos antes para hacer un borde bonito (opcional):
public DomEcho02()
{
// Make a nice border
EmptyBorder eb = new EmptyBorder(5,5,5,5);
BevelBorder bb = new BevelBorder(BevelBorder.LOWERED);
CompoundBorder cb = new CompoundBorder(eb,bb);
this.setBorder(new CompoundBorder(cb,eb));
} // Constructor
Luego, creamos un árbol vacío y lo ponemos en un JScrollPane para que los usuarios puedan ver sus contenidos cuando se agrande:
public DomEcho02()
{
...
// Set up the tree
JTree tree = new JTree();
// Build left-side view
JScrollPane treeView = new JScrollPane(tree);
treeView.setPreferredSize(
new Dimension( leftWidth, windowHeight ));
} // Constructor
Ahora creamos un JEditPane no editable que eventualmente contendrá los contenidos apuntados por los nodos seleccionados del JTree:
public DomEcho02()
{
....
// Build right-side view
JEditorPane htmlPane = new JEditorPane("text/html","");
htmlPane.setEditable(false);
JScrollPane htmlView = new JScrollPane(htmlPane);
htmlView.setPreferredSize(
new Dimension( rightWidth, windowHeight ));
} // Constructor
Con el lado izquierdo JTree y el lado derecho JEditorPane construidos, creamos un JSplitPane para contenerlos:
public DomEcho02()
{
....
// Build split-pane view
JSplitPane splitPane = new JSplitPane( JSplitPane.HORIZONTAL_SPLIT,
treeView,
htmlView );
splitPane.setContinuousLayout( true );
splitPane.setDividerLocation( leftWidth );
splitPane.setPreferredSize(
new Dimension( windowWidth + 10, windowHeight+10 ));
} // Constructor
Con este código, configuramos el JSplitPane con un divisor vertical. Eso produce una división horizontal entre el árbol y el panel de edición. También seleccionamos la posición del divisor para que el árbol tenga la anchura que prefiera, y el resto de la ventana se le asigna al panel de edición.
Finalmente, especificamos la distribución del panel y le añadimos el split pane:
public DomEcho02()
{
...
// Add GUI components
this.setLayout(new BorderLayout());
this.add("Center", splitPane );
} // Constructor
¡Felicidades! el programa es ahora una aplicación GUI. Podemos ejecutarlo ahora para ver como se ve la distribución general en la pantalla. Para referencia, aquí está el constructor completo:
public DomEcho02()
{
// Make a nice border
EmptyBorder eb = new EmptyBorder(5,5,5,5);
BevelBorder bb = new BevelBorder(BevelBorder.LOWERED);
CompoundBorder cb = new CompoundBorder(eb,bb);
this.setBorder(new CompoundBorder(cb,eb));
// Set up the tree
JTree tree = new JTree();
// Build left-side view
JScrollPane treeView = new JScrollPane(tree);
treeView.setPreferredSize(
new Dimension( leftWidth, windowHeight ));
// Build right-side view
JEditorPane htmlPane = new JEditorPane("text/html","");
htmlPane.setEditable(false);
JScrollPane htmlView = new JScrollPane(htmlPane);
htmlView.setPreferredSize(
new Dimension( rightWidth, windowHeight ));
// Build split-pane view
JSplitPane splitPane = new JSplitPane( JSplitPane.HORIZONTAL_SPLIT,
treeView,
htmlView );
splitPane.setContinuousLayout( true );
splitPane.setDividerLocation( leftWidth );
splitPane.setPreferredSize(
new Dimension( windowWidth + 10, windowHeight+10 ));
// Add GUI components
this.setLayout(new BorderLayout());
this.add("Center", splitPane );
} // Constructor
Crear Adaptadores para Mostrar el DOM en un JTree
Ahora que tenemos un marco GUI para mostrar un JTRee, el siguiente paso es obtener el JTree para mostrar el DOM. Pero el JTree quiere mostrar un TreeModel. Un DOM es un árbol, pero no es un TreeModel. Por eso necesitaremos crear una clase adaptador que haga que el DOM se parezca como un TreeModel para un JTree.
Ahora, cuando el TreeModel pase un nodo al JTree, este usa la función toString sobre dichos nodos para mostrar el texto que hay en el árbol. La función toString estándard no va muy bien, por eso necesitaremos envolver los nodos DOM en un AdapterNode que devuelva el texto que queremos. Lo que el TreeModel le da al JTree, entonces, será de hecho un objeto AdapterNode que envuelva nodos DOM.
|
Nota:
Las clases que siguen están definidas como clases internas. Si estamos codificando para la plataforma 1.1, necesitaremos definir estas clases como clases externas. |
Definir la Clase AdapterNode
Empezamos importanto, el tree, el event, y las clases de utilidades que vamos a necesitar para hacer este trabajo.
// For creating a TreeModel
import javax.swing.tree.*;
import javax.swing.event.*;
import java.util.*;
public class DomEcho02 extends JPanel
{
Nos movemos a la parte final del programa, y definimos un conjunto de strings para los tipos de elementos de los nodos:
...
} // makeFrame
// An array of names for DOM node-types
static final String[] typeName = {
"none",
"Element",
"Attr",
"Text",
"CDATA",
"EntityRef",
"Entity",
"ProcInstr",
"Comment",
"Document",
"DocType",
"DocFragment",
"Notation",
};
} // DomEcho
Estas son las cadenas que se mostrarán en el JTree. Las especificaciones de estos tipos de nodos puede encontrarse en los comentarios de la clase org.w3c.dom.Node
Luego, definimos la envoltura AdapterNode para los nodos DOM:
static final String[] typeName = {
...
};
public class AdapterNode
{
org.w3c.dom.Node domNode;
// Construct an Adapter node from a DOM node
public AdapterNode(org.w3c.dom.Node node) {
domNode = node;
}
// Return a string that identifies this node in the tree
// *** Refer to table at top of org.w3c.dom.Node ***
public String toString() {
String s = typeName[domNode.getNodeType()];
String nodeName = domNode.getNodeName();
if (! nodeName.startsWith("#")) {
s += ": " + nodeName;
}
if (domNode.getNodeValue() != null) {
if (s.startsWith("ProcInstr"))
s += ", ";
else
s += ": ";
// Trim the value to get rid of NL's at the front
String t = domNode.getNodeValue().trim();
int x = t.indexOf("\n");
if (x >= 0) t = t.substring(0, x);
s += t;
}
return s;
}
} // AdapterNode
} // DomEcho
Esta clase declara una variable que contiene el nodo DOM, y requiere ser especificada como un argumento del constructor. Luego define la operación toString, lo que devuelve el tipo de nodo desde el array de Strings, y luego lo añade a esta información adicional sobre el nodo, para identificarlo posteriormente.
Como podemos ver en la tabla de tipos de nodos de org.w3c.dom.Node, cada nodo tiene un tipo, un nombre y un valor, que podría o no estar vacío. En aquellos casos en que el nombre del nodo empieza con "#", este campo duplica el tipo de nodo, por eso está a punto de incluir. Esto explica las líneas que dicen.
if (! nodeName.startsWith("#")) {
s += ": " + nodeName;
}
El resto del método toString merece un par de notas. Por ejemplo, estas líneas.
if (s.startsWith("ProcInstr"))
s += ", ";
else
s += ": ";
sólo proporcionan un poco de "azucar sintáctica". El campo tipo de una Instrucción de Procesamiento termina con dos puntos (:), por eso estos código se mantienen para doblar los dos puntos.
Las otras líneas interesantes son:
String t = domNode.getNodeValue().trim();
int x = t.indexOf("\n");
if (x >= 0) t = t.substring(0, x);
s += t;
Estas línea recortan el campo valor al primer caracter de nueva línea en el campo. Si dejamos fuera estas líneas, veremos algunos caracteres muy divertidos en el JTree.
|
Nota:
Recuerda que XML estipula que todos los finales de línea están normalizados a nuevas líneas sin importar el sistema de donde vienen los datos. Esto hace la programación un poco más sencilla. |
Envolver un DomNode y devolver el string deseado son las principales funciones del AdapterNode. Pero como el adaptador TreeModel necesita responder a cuestiones como "¿Cuántos hijos tiene este nodo?", y satisfacer comandos como "Dame el número de hijos de este nodo", nos ayudaría definir unos cuantos métodos de utilidad. (El adaptador podría siempre acceder al nodo DOM y obtener esta información por sí mismo, pero de esta forma, las cosas están más encapsuladas).
Añadimos el código en negrita de abajo para devolver el índice de un hijo especificado. El hijo que corresponde a un índice dado, y el contador de nodos hijos.
public class AdapterNode
{
...
public String toString() {
...
}
public int index(AdapterNode child) {
//System.err.println("Looking for index of " + child);
int count = childCount();
for (int i=0; i<count; i++) {
AdapterNode n = this.child(i);
if (child == n) return i;
}
return -1; // Should never get here.
}
public AdapterNode child(int searchIndex) {
//Note: JTree index is zero-based.
org.w3c.dom.Node node =
domNode.getChildNodes().item(searchIndex);
return new AdapterNode(node);
}
public int childCount() {
return domNode.getChildNodes().getLength();
}
} // AdapterNode
} // DomEcho
Definir el Adaptador TreeModel
Ahora, por último, estamos preparados para escribir el adaptador TreeModel. Una de las cosas bonitas del modelo JTree es lo relativamente fácil que es convertir un árbol existente para mostrarlo. Una de las razones para esto es la clara separación entre la vista mostrable que usa el JTree, y la vista modificable, que usa la aplicación. El punto importante es que para satisfacer el interface TreeModel sólo necesitamos (a) proporcionar métodos para acceder e informar sobre los hijos y (b) registrar el oyente JTree apropiado, para que sepa actualizar su vista cuando el modelo cambie.
Añadimos el código en negrita de abajo para crear el adaptador TreeModel y especificar los métodos de procesamiento de hijos.
...
} // AdapterNode
// This adapter converts the current Document (a DOM) into
// a JTree model.
public class DomToTreeModelAdapter implements
javax.swing.tree.TreeModel
{
// Basic TreeModel operations
public Object getRoot() {
//System.err.println("Returning root: " +document);
return new AdapterNode(document);
}
public boolean isLeaf(Object aNode) {
// Determines whether the icon shows up to the left.
// Return true for any node with no children
AdapterNode node = (AdapterNode) aNode;
if (node.childCount() > 0) return false;
return true;
}
public int getChildCount(Object parent) {
AdapterNode node = (AdapterNode) parent;
return node.childCount();
}
public Object getChild(Object parent, int index) {
AdapterNode node = (AdapterNode) parent;
return node.child(index);
}
public int getIndexOfChild(Object parent, Object child) {
AdapterNode node = (AdapterNode) parent;
return node.index((AdapterNode) child);
}
public void valueForPathChanged(TreePath path, Object newValue) {
// Null. We won't be making changes in the GUI
// If we did, we would ensure the new value was really new
// and then fire a TreeNodesChanged event.
}
} // DomToTreeModelAdapter
} // DomEcho
En este código, el método getRoot devuelve el nodo raíz del DOM, lo envuelve como un objeto AdapterNode. A partir de aquí, todos los nodos devueltos por el adaptador serán AdapterNodes que envuelven nodos DOM. Se hara lo mismo siempre que el JTree pida los hijos de un padre dado, el número de hijos que tiene un padre, etc., al JTree le pasaremos un AdapterNode. Sabemos que, como controlamos todos lo nodos que ve el JTree, empieza con el nodo raíz.
JTree usa el método isLeaf para determinar si mostrar o no un icono de expandible/contraible a la izquierda del nodo, por eso este método devuelve true sólo si el nodo tiene hijos. En este método, vemos el forzado del objeto genérico que JTree nos envía al objeto AdapterNode que nosotros conocemos. *Nosotros* sabemos que nos está enviando un objeto adaptador, pero el interface, por ser general, define objetos, por eso tenemos que hacer el forzado.
Los siguientes tres métodos devuelven el número de hijos de un nodo dado, el hijo que vive en un índice dado, y el índice de un hijo dado, respectivamente.
El último método se invoca cuando el usuario cambia un valor almacenado en el JTree. En esta aplicación, no lo soportamos. Pero si fuera así, la aplicación tendría que hacer el cambio en el modelo oculto y luego informar a los oyentes de que el cambio ha ocurrido.
Para informar a los oyentes de que ha ocurrido un cambio, necesitaremos la habilidad de registrarlos. Esto nos trae los dos últimos métodos necesarios del interface TreeModel. Añadimos el código en negrita de abajo para definirlos.
public class DomToTreeModelAdapter ...
{
...
public void valueForPathChanged(TreePath path, Object newValue) {
...
}
private Vector listenerList = new Vector();
public void addTreeModelListener( TreeModelListener listener ) {
if ( listener != null && ! listenerList.contains( listener ) ) {
listenerList.addElement( listener );
}
}
public void removeTreeModelListener( TreeModelListener listener ) {
if ( listener != null ) {
listenerList.removeElement( listener );
}
}
} // DomToTreeModelAdapter
Como esta aplicación no hará cambios en el árbol, esto métodos no se usarán, por ahora. Sin embargo, lo haremos en el futuro, cuando los necesitemos.
|
Nota:
Este ejemplo usa Vector por eso todavía funciona en aplicaciones 1.1. Si estamos codificando en 1.2 o posteriores, podríamos usar las collections en su lugar. private LinkedList listenerList = new LinkedList(); |
Las operaciones de lista son add y remove. Para iterar sobre la lista, como en las siguientes operaciones, usamos.
Iterator it = listenerList.iterator();
while ( it.hasNext() ) {
TreeModelListener listener = (TreeModelListener)it.next();
...
}
Aquí también tenemos algunos métodos opcionales que no usaremos en esta aplicación. En este punto, hemos construido una plantilla razonable para un adaptador TreeModel.
public void removeTreeModelListener( TreeModelListener listener ) {
...
}
public void fireTreeNodesChanged( TreeModelEvent e ) {
Enumeration listeners = listenerList.elements();
while ( listeners.hasMoreElements() ) {
TreeModelListener listener =
(TreeModelListener)listeners.nextElement();
listener.treeNodesChanged( e );
}
}
public void fireTreeNodesInserted( TreeModelEvent e ) {
Enumeration listeners = listenerList.elements();
while ( listeners.hasMoreElements() ) {
TreeModelListener listener =
(TreeModelListener)listeners.nextElement();
listener.treeNodesInserted( e );
}
}
public void fireTreeNodesRemoved( TreeModelEvent e ) {
Enumeration listeners = listenerList.elements();
while ( listeners.hasMoreElements() ) {
TreeModelListener listener =
(TreeModelListener)listeners.nextElement();
listener.treeNodesRemoved( e );
}
}
public void fireTreeStructureChanged( TreeModelEvent e ) {
Enumeration listeners = listenerList.elements();
while ( listeners.hasMoreElements() ) {
TreeModelListener listener =
(TreeModelListener)listeners.nextElement();
listener.treeStructureChanged( e );
}
}
} // DomToTreeModelAdapter
|
Note:
Estos métodos se han tomado de la clase TreeModelSupport descrita en Entender el TreeModel. Esta arquitectura fue producida por Tom Santos y Steve Wilson, y es mucho más elegante que la que hemos creado aquí. |
Finalizarlo
En este momento, está básicamente finalizado. Todo lo que necesitamos es saltar de nuevo al constructor y añadir el código para construir un adaptador y entregarselo al JTree como el TreeModel.
// Set up the tree JTree tree = new JTree(new DomToTreeModelAdapter());
Ahora podemos compilar y ejecutar el código sobre un fichero XML. En la siguiente página, exploraremos lo que veremos al hacer esto.
Me ha aclarado muchas ideas
















































