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.