Unir un Esquema a las Clases
Este capítulo demuestra cómo usar JAXB para unir un DTD a un conjunto de clases Java.
Para generar la clases, realizamos estos pasos:
- Escribir un esquema de unión, que contiene las instrucciones de cómo unir un DTD a las clases.
- Ejecutar el compilador de esquema con el DTD y el esquema de unión como entradas para generar el código fuente.
- Compilar el código fuente para generar las clases.
Los dos capítulos siguientes muestran cómo utilizar las clases para construir representaciones de datos desde documentos XML
y trabajar con los datos. El ejemplo que estos capítulos usan es la sencilla aplicación de libro de cheques que se describió en
el escenario 1: Balancear un Libro de Cheques. Con esta aplicación, podremos registrar transacciones en un libro de cheques y
determinar el balance de la cuenta controlada.
El ejemplo checkbook usa el
checkook.dtd, que está localizado en el directorio
examples/checkbook de nuestra instalación de JAXP. La primera sección de
este capítulo explica el DTD checkbook.
El DTD de Ejemplo: "checkook.dtd"
Antes de crear la aplicación checkbook, deberíamos entender el DTD en el que está basada. Esta sección
describe brevemente el checkook.dtd
mostrado aquí:
<!ELEMENT checkbook ( transactions, balance ) >
<!ELEMENT transactions ( deposit | check | withdrawal )* >
<!ELEMENT deposit ( date, name, amount )>
<!ATTLIST deposit category ( salary | interest-income | other ) #IMPLIED >
<!ELEMENT check ( date, name, amount, ( pending | void | cleared ), memo? ) >
<!ATTLIST check number CDATA #REQUIRED category ( rent | groceries | other ) #IMPLIED >
<!ELEMENT withdrawal ( date, amount ) >
<!ELEMENT balance (#PCDATA) >
<!ELEMENT date (#PCDATA ) >
<!ELEMENT name (#PCDATA) >
<!ELEMENT amount (#PCDATA) >
<!ELEMENT memo (#PCDATA) >
<!ELEMENT pending EMPTY >
<!ELEMENT void EMPTY >
<!ELEMENT cleared EMPTY >
Un documento XML debe tener exactamente un elemento de raíz, pero el documento puede seleccionar que elemento
utilizar como su elemento raíz del DTD. Para nuestro ejemplo del checkbook, tanto
checkbook como transactions se pueden utilizar como elementos raíz por
documentos XML de este ejemplar de DTD. La sección Crear el Esquema de Unión Mínimo
Requerido muestra cómo especificar en el esquema de unión qué elementos de un documento XML válido pueden utilizarse
como elementos raíz. Según el DTD, un checkbook contiene transacciones
que se hacen contra la cuenta y un balance que representa la cantidad de dinero que la cuenta contiene.
El conjunto de transacciones, según la definición del elemento de las transacciones, contiene cero o más depósitos, cheques, o reintegros.
Una transación deposit consta de:
- La fecha en que se hizo.
- El nombre de la persona o empresa que proporcionó el dinero para el depósito.
- La cantidad depositada.
El elemento deposit también tiene un atributo, llamado category,
que describe la razón del depósito.
Una transación check consta de :
- La fecha en la que se firmo el cheque.
- El nombre de la persona o empresa que recibe el cheque.
- La cantidad del cheque.
- El estado del cheque: cleared, void, o
pending (todavía no se ha cobrado).
- Un memo opcional.
El check también tiene dos atributos: number y
category. El atributo number representa el número de cheque. El
atributo category representa el motivo por el que se ha extendido el cheque.
La transación withdrawal (reintegro) consta sólo de la fecha en la que se hizo y la
cantidad reintegrada.
Los elementos balance , date , name ,
amount , y memo se han definido para tener contenido de carácter.
Este capítulo muestra cómo generar varios tipos para el contenido de los elementos balance,
data y amount.
Los elementos pending , void , y cleared
no tienen contenido, por lo que se han definido como EMPTY.
Escribir el Esquema de Unión
No necesitamos proporcionar una declaración de unión para cada componente del DTD. El compilador de esquema asume declaraciones de
uniones por defecto si no se proporciona una unión particular. Sin embargo, si no estámos satisfechos con las uniones por defecto,
necesitamos proporcionar declaraciones de unión en el esquema de unión para generar las clases que deseamos.
Esta sección nos muestra:
- Cómo escribir el esquema de unión mínimo, que es un esquema de unión que contiene las declaraciones mínimas que permitirán
al compilador de esquema generar las clases.
- Qué código generará el compilador de esquema basándose en el esquema de unión mínimo y el DTD.
- Cómo añadir declaraciones al esquema de unión mínimo para que el compilador de esquema genere el código que queremos,
no sólo el código por defecto.
Esta guía explica detalladamente cómo escribir el esquema de unión. El esquema de unión es lo que utilizamos para controlar qué
tipo de código genera el compilador de esquema. Por lo tanto, es importante que entendamos cómo el compilador de esquema interpreta
las declaraciones que proporcionamos en el esquema de unión y lo que el compilador de esquema asume si no proporcionamos las
declaraciones de unión para una declaración del DTD. Además, el lenguaje de unión y el compilador de esquema que interpreta el
esquema de unión son los suficientemente proderosos como para hacer asunciones razonables, pero todavía nos permiten mucha
flexibilidad para definir cómo el DTD está unido a las clases.
Aunque el compilador del esquema puede producir una unión razonable en el caso simple, cuando veamos qué clase de código genera
el compilador de esquema basandose en el DTD checkbook y un esquema de unión mínimo, entenderemos la
importancia de proporcionar declaraciones de unión de modo que podamos generar el código apropiado para nuestra aplicación.
Crear el Esquema de Unión Mínimo Requerido
Tanto si deseamos o no validar las declaraciones de unión por defecto asumidas por el compilador de esquema, necesitamos
proporcionar un esquema de unión. Para crear el esquema de unión para el DTD del checkbook:
- Creamos un nuevo fichero de texto llamado
checkook.xjs.
- En checkook.xjs, escribimos:
<xml-java-binding-schema version="1.0ea">
Esta etiqueta identifica el fichero como un esquema de unión.
- Todos los esquemas de unión deben declarar al menos un elemento raíz. En nuestro ejemplo, queremos declarar dos elementos
raíz: checkbook y transactions. Para declarar los elementos raiz,
escribimos:
<element name=checkbook type=class root=true />
<element name=transactions type=class root=true />
Para declarar estos elementos raíz, utilizamos la declaración de unión del elemento para unir un tipo de elemento a una clase.
El valor del atributo debe ser el nombre del elemento tal como aparece en el DTD. El valor del tipo de atributo es
class en este caso porque éstos son elementos raíz y se deben limitar a las clases.
El atributo root debe ser igual a true porque queremos que los
ejemplares del documento XML del DTD checkbook pudieran declarar checkbook
o transactions como elementos raíz.
- Introducimos la etiqueta final para el elemento xml-java-binding-schema:
</xml-java-binding-schema>
Ahora tenemos un esquema de unión legal desde el cual el compilador de esquema puede generar clases.
De acuerdo con el DTD, el compilador de esquema hace asunciones con respecto a cómo unir las otras declaraciones del DTD
para los cuales no proporcionamos declaraciones de unión. La siguiente sección explica las declaraciones de unión asumidas por
el compilador de esquema basándose en el DTD checkbook y el esquema de unión actual.
Una vez que entendamos qué tipo de código se produce desde un esquema de unión mínimo, la escritura el resto del esquema de
unión es fácil.
Entender las Declaraciones por Defecto
Las declaraciones de unión que el compilador de esquema asume basándose en el DTD checkbook
y el esquema de unión mínimo son:
<element name="checkbook" type="class" root=true>
<content>
<element-ref name="transactions"/>
<element-ref name="balance"/>
</content>
</element>
<element name="transactions" type="class" root=true>
<content property="content"/>
</element>
<element name="deposit" type="class">
<attribute name="category"/>
<content>
<element-ref name="date"/>
<element-ref name="name"/>
<element-ref name="amount"/>
</content>
</element>
<element name="check" type="class">
<attribute name="number"/>
<attribute name="category"/>
<content property="content"/>
</element>
<element name="withdrawal" type="class">
<content>
<element-ref name="date"/>
<element-ref name="amount"/>
</content>
</element>
<element name="balance" type="value"/>
<element name="date" type="value"/>
<element name="name" type="value"/>
<element name="amount" type="value"/>
<element name="memo" type="value"/>
<element name="pending" type="class"/>
<element name="void" type="class"/>
<element name="cleared" type="class"/>
El resto de esta sección explica cada una de estas declaraciones de union.
Declaración de Unión de Elementos
El compilador de esquema asume diversas declaraciones de unión para los elementos dependiendo de qué tipo de contenido tienen
o si tienen atributos. Para los elementos simples, que tienen solamente el contenido del tipo carácter y ningun atributo,
el compilador de esquema asume que los elementos están unidos a las propiedades dentro de la clase de su elemento padre.
Los elementos balance, date, name
y amount son elementos simples y por eso el compilador de esquema asume estas uniones para ellos:
<element name="balance" type="value"/>
<element name="date" type="value"/>
<element name="name" type="value"/>
<element name="amount" type="value"/>
<element name="memo" type="value"/>
El nombre de los atributos debe ser el nombre del elemento tal como aparece en el DTD. El tipo del atributo es
value en estos casos porque estos elementos están unidos a propiedades, no a clases.
Éstas declaraciones de unión harán que compilador de esquema genere estas propiedades:
String getBalance();
void setBalance(String x);
String getDate();
void setDate(String x);
String getName();
void setName(String x);
String getAmount();
void setAmount(String x);
String getMemo();
void setMemo(String x);
Una propiedad String está muy bien para los elementos name
y memo. Sin embargo, para balance y
amount, necesitamos un tipo que represente mejor valores de moneda. De forma similar
date se debe limitar a una propiedad que acepte y devuelva una cierta clase de tipo que
represente mejor una fecha. La sección Especificar Tipos
mostrará cómo generar las propiedades que aceptan y devuelven diferentes tipos.
Para el resto de los tipos de elementos, el compilador de esquema asume que los elementos están unidos a clases.
Si un elemento contiene cualquier cosa distinta de contenido de caracteres o tiene atributos, el compilador de esquema
lo unirá a una clase. Los elementos deposit, check, y
withdrawal tienen elementos de contenido, y deposit y
check tiene atributos. Estos elementos también están unidos a clases. Las uniones
por defecto para estos elementos son:
<element name="deposit" type="class" >
<element name="check" type="class" >
<element name="withdrawal" type="class" >
Observa que no necesitamos especificar el atributo root como hicimos en la sección
Crear el Esquema de Unión Mínimo Requerido. El compilador de esquema
asume que el valor de este atributo es false. Sólo necesitamos especificar el valor
de este atributo como true si queremos el ejemplar del documento XML tenga la
posibilidad de usar ese elemento como elemento raíz. De la declaración de unión del elemento
deposit el compilador de esquema genera esta definición de clase y este constructor:
public class Deposit extends MarshallableObject {
public void Deposit();
Las clases Check , y Withdrawal se parecerán mucho a ésta.
Los elementos pending , void , y
cleared tienen contenido EMPTY y están includos en un modelo de
grupo de elección, y por eso están unidos a clases, como se especifica en sus declaraciones de tipos de atributos:
<element name="pending" type="class"/>
<element name="void" type="class"/>
<element name="cleared" type="class"/>
Declaración de Unión de Atributos
El DTD checkbook define tres atributos. El elemento deposit
tiene un atributo category, y el elemento check tiene un
atributo number y también un atributo category:
<!ELEMENT deposit ...
<!ATTLIST deposit category ( salary | interest-income | other ) #IMPLIED >
<!ELEMENT check ...
<!ATTLIST check number CDATA #REQUIRED category ( rent | groceries | other ) #IMPLIED >
Todos estos atributos toman valores atómicos, en vez de valores compuestos, y por eso el compilador de esquema
asume que estos atributos están unidos a propiedades String, como se especifica
en esta declaraciones de unión por defecto:
<element name=deposit type=class >
<attribute name=category />
...
<element name="check" type="class"> <attribute name=number />
<attribute name=category />
...
Dentro de la clase Deposit, el compilador de esquema genera esta propiedad para
representar el atributo category:
void setCategory(String x);
String getCategory();
Dentro de la clase Check, el compilador de esquema genera estas propiedades para
representar a los atributos number y category:
void setNumber(String x);
String getNumber();
void setCategory(String x);
String getCategory();
Observa que la propiedad number acepta y devuelve un String.
La sección Especificar Tipos mostrará cómo modificar el esquema para
requisitos de uniones particulares de modo que el compilador de esquema genere una propiedad
number que acepte y devuelva un int. Asimismo, la sección
Crear Tipos Enumerados mostrará cómo generar un tipo enumerado para la
propiedad category.
Declaración de Unión de Contenido
El declaración de unión de contenido es lo más complicado, reflejando el número infinito de maneras en que podemos
especificar el contenido XML, pero JAXB lo hace fácil para nosotros. El tipo más común de modelo
de contenido es una secuencia simple, no-repetitiva, por ejemplo (a, b, c, d). Si utilizamos este tipo de modelo de
contenido, muy probablemente no necesitaremos especificar un declaración de unión para él porque el compilador de
esquema genera una propiedad separada para cada elemento de la secuencia, que es generalmente lo que deseamos.
La mayoría de los elementos que hay en
checkook.dtd tienen
contenido secuencial simple y no repetitivo. Para estos elementos, el compilador de esquema asume las
declaraciones de unión mostradas en negrita:
<element name="checkbook" type="class" root=true>
<content>
<element-ref name="transactions"/>
<element-ref name="balance"/>
</content>
</element>
<element name="deposit" type="class">
<attribute name="category"/>
<content>
<element-ref name="date"/>
<element-ref name="name"/>
<element-ref name="amount"/>
</content>
</element>
<element name="withdrawal" type="class">
<content>
<element-ref name="date"/>
<element-ref name="amount"/>
</content>
</element>
El declaración de unión element-ref se utiliza para unir un ejemplar de un elemento en un
modelo de contenido a una propiedad en la clase del elemento padre. La declaración del elemento que corresponde al ejemplar
del elemento une el elemento a sí mismo, incluyendo la unión del elemento a su tipo. Por defecto, el compilador de esquema
genera propiedades String desde todas las declaraciones de uniones
element-ref que se refieran a elementos simples. La propiedad por defecto para el elemento
name es:
public class Deposit {
...
String getName();
void setName(String x);
El compilador de esquema también genera una propiedad String para
balance, date, y amount.
La clase Checkbook contiene una propiedad para el elemento
balance. Las clases Deposit y
Withdrawal tienen propiedades para los elementos date y
amount. En la sección
Especificar Tipos veremos que los tipos de estas propiedades
cambian cuando utilizamos el atributo convert en las declaraciones de unión correspondientes
al elemento.
Como especificamos que el elemento transactions está unido a una clase, la declaración de
unión element-ref para el elemento transactions hará
que el compilador de esquema genere esta propiedad en el clase Checkbook:
Transactions getTransactions();
void setTransactions(Transactions x);
Si un elemento contiene algo distinto a una secuencia simple no repetitiva, el compilador de esquema asumirá la declaración de
unión de la propiedad general-content, que es:
<content property=content />
Los elementos transactions y check no tienen secuencias
simples y no repetitivas como contenidos, por eso el compilador de esquema asume estas declaraciones de unión para ellos:
<element name="transactions" type="class" root=true>
<content property="content"/>
</element>
...
<element name="check" type="class">
<attribute name="number"/>
<attribute name="category"/>
<content property="content"/>
</element>
Se usa la declaración general-content para unir un modelode grupo completo, incluyendo modelos
de grupos anidados, a una propiedad. Este declaración no es muy útil para nuestros propósitos porque deseamos tener acceso
a los elementos individuales en estos modelos de contenido. Esta declaración es útil para definir uniones más flexibles si
anticipamos que nuestro DTD cambiará en el futuro. Para más información sobre esta declaración, puedes ver la sección
Manejar la Evolución del Esquema.
Desde estas declaraciones de unión, el compilador de esquema genera esta propiedad en las clases
Transactions y Check:
List getContent();
void emptyContent();
void deleteContent();
El método getContent devuelve una lista modificable que contiene el valor
actual de las propiedades. El método emptyContent descarta los valores
de la lista y crea una nueva lista, vacía. El método deleteContent borra la lista.
Personalizar el Esquema de Unión
Ahora que entendemos las declaraciones de unión que el compilador del esquema asumirá basándose en el DTD y el
esquema de unión mínimo, es fácil escribir el esquema de unión: Todo lo que necesitamos escribir son las declaraciones
de uniones que no son asumidas por el compilador de esquema. Esta sección explica cada una de las personalizaciones
para requisitos particulares que podemos hacer al esquema de unión para conseguir las clases que deseamos.
El esquema de unión que usaremos para la aplicación checkbook es:
<xml-java-binding-schema version="1.0ea">
<element name="checkbook" type="class" root="true" />
<element name="transactions" type="class" root="true">
<content>
<choice property="entries" collection="list" supertype="Entry" />
</content>
</element>
<element name="balance" type="value" convert="BigDecimal"/>
<element name="amount" type="value" convert="BigDecimal" />
<element name="date" type="value" convert="TransDate" />
<element name="deposit" type="class" >
<attribute name="category" convert="DepCategory" />
</element>
<element name="check" type="class" >
<content>
<element-ref name="date"/>
<element-ref name="name" />
<element-ref name="amount" />
<choice property="pend-void-clrd"/>
</content>
<attribute name="number" convert="int" />
<attribute name="category" convert="CheckCategory" />
</element>
<conversion name="BigDecimal" type="java.math.BigDecimal" />
<conversion name="TransDate" type="java.util.Date" parse="TransDate.parseDate"
print="TransDate.printDate" />
<enumeration name="DepCategory" members="salary interest-income other"/>
<enumeration name="CheckCategory" members="rent groceries other"/>
<interface name="Entry" members="Deposit Check Withdrawal" properties="date amount" />
</xml-java-binding-schema>
Esta sección nos lleva a través de este esquema de unión como si lo hubieramos escrito desde él principio. Ya hemos
incorporado las uniones del elemento de raíz en la sección Crear el Esquema de
Unión Mínimo Requerido, por eso empecemos con ella:
- Reemplazamos la etiqueta de elemento vacío (/>) con un angulo a la derecha (>) y añadimos una etiqueta final para
la declaración de elemento raíz transactions porque le vamos a añadir declaraciones de
contenido:
<xml-java-binding-schema version="1.0ea">
<element name="checkbook" type="class" root="true" />
<element name="transactions" type="class" root="true" >
</element>
- Antes de que empezar a escribir la declaración de unión personalizada, las primeras declaraciones que necesitamos en nuestro
esquema de unión son las declaraciones de unión de elemento para los elementos deposit y
check porque especificaremos tipos para sus atributos y personalizaremos el modelo de contenido
del elemento check . El compilador de esquema necesita estas declaraciones para poder generar
las propiedads para los atributos y el contenido de elección en la clase correcta y para los elementos correctos.
Detntro del elemeno raíz de nuestro esquema de unión, después de las declariones de unión de elementos raíces, introducimos:
<element name=deposit type=class >
</element>
...
<element name=check type=class >
</element>
Más tarde añadiremos el atributo personalizado y las declaraciones de unión de contenidos dentro de estos elementos.
Especificar Tipos
Por defecto, el compilador de esquema genera métodos get que devuelven un
String y métodos set que aceptan un
String para todos los elementos y atributos simples. Por ejemplo, consideremos estas
declaraciones de unión por defecto:
<element-ref name=amount />
...
<element name=amo unt type=value />
Desde estas declaraciones de unión, el compilador de esquema genera estos dos métodos:
public String getAmount();
public void setAmount(String amount);
Si deseamos realizar algunos cálculos con amount, necesitamos convertirla de un
String a un algún otro tipo que permita que lo utilicemos en un cálculos. Para los
cálculos que implican valores de moneda, el tipo BigDecimal es una buena opción
porque representa números decimales con signo de precisión arbitraría, y la clase
BigDecimal proporciona los métodos para aritmética básica.
Para especificar un tipo, utilizamos el declaración de conversión para definir la conversión y el atributo
convert de la declaración de elemento o de atributo, dependiendo de si estámos
convirtiendo el tipo de una propiedad de elemento o de una propiedad de atributo, para referenciar la declaración
de conversión.
Especificar Tipos No Primitivos
Para definir una conversión de String a BigDecimal:
- Añadimos esta declaración de conversión en cualquier lugar en el nivel superior (entre de las etiquetas
<xml-java-binding-schema version="1.0ea">) de nuestro esquema de unión, quizás después
de la declaración de unión del elemento check:
<conversion name=BigDecimal type=java.math.BigDecimal />
Cualquier declaración de unión de elemento o de atributo que utilice esta conversión se refiere a ella por el nombre
BigDecimal, según lo especificado por el nombre del atributo. El valor del tipo de atributo
es el tipo real al cual se convierte una propiedad.
- Para instruir al compilador de esquema para que genere una propiedad amount con un
tipo BigDecimal:
Estas declaraciones de unión producen estás firmas de métodos:
public java.math.BigDecimal getAmount();
public void setAmount(java.math.BigDecimal amount);
Declarar por separado la conversión de una unión de elemento nos permite reutilizar una conversión con otras uniones de elemento.
Podemos hacer esto con el elemento balance, que también necesita estar unido a una propiedad
BigDecimal.
Para instruir al compilador de esquema para que genere una propiedad balance con un tipo
BigDecimal:
- >Añadimos una declaración de unión de elemento para el elemento amount en la parte
superior de nuestro esquema de unión, y asignamos BigDecimal a su atributo
convert:
<element name=balance type=value convert=BigDecimal />
También podemos convertir el tipo de la propiedad del elemento date a un
java.util.Date. Puesto que una fecha se puede escribir de muchas formas distintas,
necesitamos especificar cómo se debe analizar la fecha cuando se desempaquete y cómo debe imprimirse cuando se
empaquete. La declaración de conversión incluye los atributos parse y
print para este propósito.
Para convertir los elementos date a un java.util.Date:
- Añadimos esta declaración de conversión al esquema de unión:
<conversion name=TransDate type=java.util.Date
parse=TransDate.parseDate print=TransDate.printDate/>
El nombre TransDate se refiere a una clase Java que necesitamos proporcionar.
Esta clase contiene un método estático parseDate que especifica cómo analizar la
fecha y un método estático printDate que especifica cómo imprimir la fecha.
-
Para instruir al compilador de esquema para que genere una propiedad date
con la clase TransDate, añadimos un atributo convert
a la declaración de unión del elemento date y seleccionamos su valor a
TransDate:
<element name=date type=value convert=TransDate />
Estas declaraciones de unión producen estas firmas de métodos:
public java.util.Date getDate();
public void setDate(java.util.Date x);
En el ejemplo de conversión BigDecimal, no necesitamos especificar un método de análisis
o de impresión porque la clase java.math.BigDecimal especifica un constructor que acepta
un String y devuelve un BigDecimal y un método
toString que acepta un BigDecimal y devuelve un
String. Para aplicar la conversión, el compilador de esquema genera código para invocar al
constructor y al método toString.
Especificar Tipos Primitivos
Cuando especificaamos tipos primitivos, tales como int, no necesitamos proporcionar una
declaración de unión de conversión separada; simplemente añadimos el atributo convert a la
declaración de unión del elemento o del atributo y asignamos el valor al tipo primitivo.
Dentro de la declaración de unión del elemento check, añadimos una declaración de unión de
atributo para el atributo number y especificamos un tipo int
para su propiedad:
<element name=check type=class >
<attribute name=number convert=int />
</element>
Crear Tipos Enumerados
El atributo convert también se puede utilizar para especificar un tipo enumerado. En el lenguaje
de programación Java, representamos tipos enumerados con un tipo seguro enum, que es una clase
cuyos ejemplares representan un conjunto fijo de valores.
Un atributo cuyo valor sólo se pueda fijar a uno de un conjunto fijo de valores es un buen candidato para un tipo enumerado.
El DTD checkbook tiene dos de estos atributos: el atributo category
del elemento deposit y el atributo category del elemento
check:
<!ATTLIST deposit ... category ( salary | interest-income | other ) #IMPLIED >
<!ATTLIST check ... category ( rent | groceries | other ) #IMPLIED >
Para generar los tipos para estos atributos:
- Introducimos dos etiqueutas enumeration para cada conversión en el nivel superior del
esquema de unión, quizás detrás de la declaración de conversión de TransDate:
<conversion name=TransDate ...
<enumeration
<enumeration
- Para el atributo name de las etiquetas enumeration,
necesitamos un nombre único para cada enumeración porque ámbas enumeraciones están al mismo nivel en el esquema de unión.
Introducimos DepCategory para la categoria de deposit
y CheckCategory para la categoría de check.
<enumeration name="DepCategory"
<enumeration name=CheckCategory
Estos nombres serán los nombres de las clases para representar los tipos seguros enums.
- Añadimos un atributo members a cada declaración de enumeración, y les asignamos
los posibles valores de cada atributo:
<enumeration name="DepCategory" members=salary interest-income other />
<enumeration name=CheckCategory members=rent groceries other />
- Añadimos las declaraciones de unión de attribute dentro de las declaraciones de
unión de los elementos deposit y check y asignamos el nombre
de la declaración de enumeración apropiada a cada atrributo convert de la declaración
de unión de attribute:
<element name=deposit ...
<attribute name=category convert=DepCategory />
...
<element name=check ...
<attribute name=category convert=CheckCategory />
El compilador de esquema generará esta clase desde la unión del atributo category de
check:
public final class CheckCategory {
public final static CheckCategory RENT;
public final static CheckCategory GROCERIES;
public final static CheckCategory OTHER;
public static CheckCategory parse(String x);
public String toString();
}
Veremos como trabajar con esta clase en el capítulo Construir Representaciones de
Datos.
Personalizar el Modelo de Declaración de Unión de Contenido
Los modelos de contenido pueden ser muy complejos, y por eso el lenguaje de unión define muchas declaraciones
diferentes de uniones para manejar diversos tipos de modelos de contenido. Definimos estas uniones con la declaración
de unión de contenido.
Podemos utilizar la declaración de unión de contenido para definir dos tipos de declaraciones de modelo de contenido:
la propiedad general-content y la propiedad de cotenido basado en modelos.
Una propiedad general-content se utiliza para unir un modelo de contenido completoo a una
propiedad. Esta declaración no se utiliza para unir cualquier cosa en el DTD checkbook,
pero es útil para definir uniones más flexibles si nos anticipamos a que el DTD cambiará en el futuro. Para más información
sobre esta declaración, puedes ir a la sección Manejar la Evolución del
Esquema.
La declaración de propiedad de contenido basado en modelo puede contener cuatro tipos de declaraciones para
especificar diferentes tipos de uniones de grupos de modelo:
- element-ref, que especifica la unión de un ejemplar element
dentro de otros elementos content. El constructor del elemento especifica la unión
del propio elemento.
- choice, que especifica la unión de un grupo de modelo de elección anidado a una propiedad.
- sequence, que especifica la unión de un grupo de modelo de elección anidado a una propiedad.
- rest, que es una declaración de unión más flexible que podemos utilizar para especificar
cualquier tipo de contenido. Puedes econtrar más información en la sección Manejar
la Evolución del Esquema.
Recordamos de la sección Entender las Declaraciones de Unión por Defecto
que el compilador de esquema asume una declaración de unión de la propiedad general-content
para los contenidos de los elemento check y transactions
porque estos elementos no tienen modelos de contenido simples, con secuencias no repetitivas. En su lugar, estos elementos
tienen grupos de modelos de elección en sus declaraciones de contenido:
<!ELEMENT transactions ( deposit | check | withdrawal )* >
...
<!ELEMENT check ( date, name, amount, ( pending | void | cleared ), memo? ) >
...
Esta sección muestra cómo utilizar la declaración de unión de elección para hacer que el compilador de esquema
genere propiedads más útiles para el contenido de estos elementos. En el caso del contenido del elemento
transactions, deseamos asignar el grupo entero a una propiedad de elección.
Para especificar la unión del contenido del elemento transactions:
- Dentro de la declaración de unión del elemento transactions, introducimos
una etiqueta content y una declaración de unión choice,
y asignamos el valor entries al atributo property:
<element name=transactions type=class root=true >
<content>
<choice property=entries
El compilador de esquema generará una propiedad llamada Entries. Por ejemplo,
el método get se llamará getEntries. La razón por la
que utilizamos el nombre entries, es porque el nombre se refiere a un declaración de interface,
que la sección Crear Interfaces nos enseñará como crear.
- Como el modelo de contenido tiene un indicador de ocurrencia *, necesitamos
unir este contenidos a una propiedad collection. Introducimos el atributo
collection y le damos el valor list:
<choice property=entries collection=list />
Una propiedad collection puede representar un array o una List.
En este caso, deberíamos unir el contenido a una List, porque ésta, al contrario que el array,
nos permite añadir más entradas durante la ejecución.
- Introducimos la etiqueta final para la declaración de unión content:
</content>
Estas declaraciones de unión producirán está propiedad en la clase Transactions:
public List getEntries();
public void deleteEntries();
public void emptyEntries();
El método getEntry devuelve la lista de entradas completa. Una vez que consigamos la
lista, podemos iterar a través de la lista como lo haríamos con cualquier lista para llegar a una entrada determinada.
La lista devuelta por getEntries es modificable: si modificamos una entrada en esta lista,
cambiará la entrada en el árbol de contenido. El método emptyEntries desecha los valores
de la lista y crea una nueva lista, vacía. El método deleteEntries borra la lista.
Para especificar la unión del grupo del modelo de alección anidado en el modelo de elección del elemento
check, dentro de la declaración de unión del elemento check:
- Introducimos las declaraciones de unión element-ref para los elementos
date, name, y amount, e
insertamos la declaración de unión de elección como se muestra en negrita en la declaración de unión del elemento
check:
<element name=check type=class >
<attribute name=number convert=int />
<attribute name=category convert=CheckCategory />
<content>
<element-ref name=date />
<element-ref name=name />
<element-ref name=amount />
<choice property=pend-void-clrd />
</content>
</element>
El valor del atributo property es el nombre de la propiedad generada. Por ejemplo,
el método get se llamará getPendVoidClrd. Si el contenido
también contiene una opción, una secuencia, o un declaración de unión de resto de contenido, necesitamos especificar las
declaraciones de unión por defecto de element-ref para los elementos que preceden a este
contenido; si no, el compilador de esquema no sabe qué elementos se han pensado para dichas declaraciones de unión.
Crear Interfaces
Puede ser que hayas notado que los elementos deposit, check,
withdrawal tienen cierto contenido común. Puede que también hayas notado que cada uno
representa una entrada en una lista de transacciones. Cuando tenemos un grupo de clases que proporcionan funciones similares
y tienen algún comportamiento y propiedades comunes, podemos utilizar un interface para capturar las semejanzas entre las
clases. En el caso de deposit, check, y
withdrawal, todas tienen los elementos date y
amount. Estos elementos serán propiedads comunes en el interface.
Para hacer que el compilador de esquema genere un inteface, con Deposit,
Check, y Withdrawal implementaremos:
- En cualquier lugar del nivel más alto de nuestro esquema de unión, quizás después de las declaraciones de enumeración,
introducimos esta declaración de interface:
<interface name=Entry members=Deposit Check Withdrawal
properties=date amount />
El atributo members representa todas las clases que implementan el interface. El atributo
properties representa el contenido común compartido por los miembros del interface.
Como los elementos deposit, check,
withdrawal ocurren en el modelo contenido de las transacciones como un grupo de elección,
necesitamos referenciar Entry desde la unión del grupo de elección. Previamente asignamos el
nombre del interface al atributo de la propiedad y al valor list del atributo
collection. Asignamos el nombre del interface al atributo supertype:
<element name=transactions type=class class=Transactions >
<content>
<choice property=entries collection=list supertype=Entry />
</content>
</element>
El atributo supertype indica una clase o interface declarada en el esquema de unión que
cada clase de elemento inclúida en la propiedad choice implementa.
Estas declaraciones de unión producirán un interface llamado Entry, que incluirá las
propiedades date y amount:
public interface Entry {
...
public int getAmount();
public void setAmount(int x);
public Date getDate();
public void setDate(Date d);
Manejar la Evolución del Esquema
Como con cualquier recurso que genere código, un desarrollador que escriba aplicaciones básadas en el código necesita
asegurarse de que el código recientemente generado no rompa las aplicaciones. Si nos anticipamos a que el DTD cambiará,
podemos utilizar las declaraciones de unión más flexibles en el lenguaje de unión para proteger la integridad de las
aplicaciones.
La manera más fácil de manejar la evolución del esquema es aceptar los uniones por defecto que el compilador de esquema
produce. Estas uniones son definiciones muy flojas de las declaraciones del DTD, y así son más flexibles a los cambios
del DTD. Por ejemplo, cualquier grupo de modelo que no consista en elementos distintos en una secuencia no-repetitiva
se unirá usando una declaración de propiedad de contenido general, que une todo el contenido a una propiedad.
Si agregaramos elementos a este grupo de modelo, estos elementos todavía serían representados por la propiedad, y las
clases no cambiarían.
Podemos usar la declaración de propiedad de contenido general para cualquier grupo de modelo. Para unir
content usando esta declaración, usamos la construcción content:
<content property=mygroup />
Esta unión generá esta propiedad:
public List getMygroup();
public void deleteMygroup();
public void emptyMygroup();
Otra declaración de unión que podemos utilizar para manejar la evolución del esquema es la declaración de unión
rest. Esta declaración de unión es similar a la declaración de la propiedad de contenido
general en que puede representar cualquier tipo de contenido. Añadiendo una declaración rest
sobre una declaración de unión de elemento content, podemos añadir otros elementos y grupos
al modelo de contenido del DTD en el futuro sin afectar a las clases generadas. Por ejemplo, podemos añadir una construcción
rest a la unión de contenido del elemento withdrawal:
<content>
...
<rest property=rest />
</content>
como ya tenemos la propiedad rest, podemos añadir otro contenido al elemento
withdrawal sin romper la aplicación. Además, todavía podemos tener acceso individualmente
al viejo contenido con las propiedads generadas por las otras declaraciones dentro de la declaración de contenido.
Solamente el nuevo contenido, definido por la propiedad rest, será representado por una
propiedad List.
Generar las Clases Java
Ahora que hemos terminado el esquema de unión, podemos ejecutar el compilador de esquema para generar las clases Java.
Esta guía asume que hemos seguido las instrucciones en las notas de liberación, situadas en el directorio
doc de la instalación y hemos configura los classpaths correctamente.
Para generar las clases Java:
- Ejecutamos el compilador de esquema con
checkook.dtd y
checkook.xjs, el esquema
de unión que hemos creado:
xjc checkook.dtd checkook.xjs
Ahora deberíamos ver los ficheros
Checkbook.java y
Entry.java en nuestro directorio
actual.
- Compilamos los ficheros fuentes en clases Java:
javac *.java
El resto de esta sección explica el código generado en estos ficheros. Si no necesitas una explicación del código,
puedes ir al siguiente capítulo Construir Representaciones de Datos para
construir árboles de contenido usando las clases.
Los Ficheros Fuente Java Generados
Esta sección explica abreviadamente algunos de los métodos y de las clases públicas generadas por el compilador de
esquema basadas en el DTD checkbook y el esquema de unión que creamos en la sección
anterior. Puesto que las clases Deposit, Check, y
Withdrawal son tan similares, entre estas clases, esta sección explica solamente la clase
Check. Asimismo, esta sección explica solamente la clase enumerada
CheckCategory, y entre las clases Pending,
void, Cleared, esta sección explica solamente la clase
pending.
El fichero Checkbook.java
El elemento checkbook del DTD transactions.dtd está unido
a la clase Checkbook cuya firma es:
public class Checkbook extends MarshallableRootElement implements RootElement
Como checkbook es un elemento raíz, la clase Checkbook
extiende MarshallableRootElement, que es la clase que representa objetos elemento
raíz que pueden ser empaquetados y desempaquetados, e implementa RootElement.
Como cada clase generada, la clase Checkbook contiene un constructor sin argumentos:
public Checkbook();
Recuerda que el elemento checkbook contiene un elemento
transactions y un elemento balance:
<!ELEMENT checkbook ( transactions, balance ) >
Estos elementos están unidos a éstas propiedades en el clase Checkbook:
// the transactions property
public void setTransactions(Transactions x);
public Transactions getTransactions();
// the balance property
public void setBalance(java.math.BigDecimal x);
public java.math.BigDecimal getBalance();
La propiedad transactions acepta y devuelve un objeto
transactions porque el elemento transactions
también está representado por una clase. La propiedad balance acepta y devuelve
un java.math.BigDecimal debido a éstas declaraciones de unión que especificamos
en la sección Especificar Tipos:
<element name="balance" type="value" convert="BigDecimal"/>
...
<conversion name="BigDecimal" type="java.math.BigDecimal" />
Aunque MarshallableRootElement define métodos marshal,
que la clase Checkbook usa por extensión, no define ningún método
unmarshal. Así, el compilador de esquema genera estos métodos
unmarshal estáticos en la clase Checkbook:
public static Checkbook unmarshal(InputStream in)
public static Checkbook unmarshal(XMLScanner xs)
public static Checkbook unmarshal(XMLScanner xs, Dispatcher d)
Cuando despempaquetamos un documento XML, podemos invocar a unmarshal(InputStream) o
a unmarshal(XMLScanner) en los que InputStream o
XMLScanner representan nuestro documento XML.
Aunque el desempaquetamiento realiza la validación por nosotros, necesitamos realizar validación después de editar
el árbol de contenido y antes de empaquetarlo en un documento XML. Para este propósito, el compilador de esquema
genera estos métodos:
public void validateThis();
public void validate();
Después de editar una parte del árbol de contenido, podemos usar validateThis
para validar el objeto editado. Antes de empaquetar el arbol de contenido a un documento XML, debemos usar
validate para validar todos el árbol de contenido.
El Fichero Transactions.java
El elemento transactions de
checkokk.dtd está
unido a la clase Transactions cuya firma es:
public class Transactions extends MarshallableRootElement implements RootElement
Al igual que checkbook , transactions es un
elemento raíz, y por eso la clase Transactions extiende
MarshallableRootElement, e implementa RootElement.
La clase Transactions contiene un constructor sin argumentos:
public Transactions();
El elemento transactions contiene cero o más elementos
deposit, check, o withdrawal:
<!ELEMENT transactions (deposit | check | withdrawal)* >
Cuando seguimos las instrucciones de Personalizar las Declaraciones de
Unión del Modelo de Contenido, especificamos que el compilador de esquema uniera el contenido del elemento
transactions a una propiedad de colección List llamada
entries. En la sección Crear Interfaces,
especificamos que el supertype de la propiedad entries
es el interface Entry:
<element name=transactions type=class class=Transactions >
<content>
<choice property=entries collection=list supertype=Entry />
</content>
</element>
El declaración de unión del interface hizo que el compilador de esquema uniera el contenido de
transactions a una lista de deposit,
check, y withdrawals. La declaración de unión del
interface hizo que el compilador de esquema generara un interface Entry, que implementaban
las clases Deposit, Check, y
Withdrawal. El interface Entry se explica en la siguiente
sección.
La propiedad entries consta de tres métodos que usamos para aceder al contenido del
elemento transactions:
public List getEntries();
public void deleteEntries();
public void emptyEntries();
El método getEntry devuelve la lista de entradas completa. Una vez que consigamos la lista,
podemos iterar a través de la lista como lo haríamos con cualquier lista para obtener una entrada determinada. La lista
devuelta por getEntries es modificable: Si cambiamos una entrada de esta lista, cambiará
la entrada en el árbol de contenido. El método deleteEntries suprime la lista de entradas
actual. El método emptyEntries suprime los valores de la lista.
Cualquier contenido de transactions (tanto si son dos depósitos o un depósito y cinco reintegros)
implementa Entry. Por lo tanto, no necesitamos realizar pruebas de instanceof o forzados de tipo
en los items de lista que representan entradas, a menos que estemos trabajando con un elemento contenido en
deposit, check, o withdrawal
que no sea un miembro del interface Entry.
Al igual que la clase Checkbook, la clase Transactions
contiene los métodos marshal, unmarshal y
validate que un usuario debería invocar.
El Fichero Entry.java
El supertype del contenido del elemento transactions
es el inteface Entry, como se especifica en esta declaración de unión de inteface:
<element name=transactions type=class class=Transactions >
<content>
<choice property=entries collection=list supertype=Entry />
</content>
</element>
La firma del inteface Entry es:
public interface Entry {
El único contenido de elementos comunes entre los elementos deposit,
check, y withdrawal son date
y amount, y por eso asignamos estos elementos al atributo
properties de la construcción del interface en la sección
Crear Interfaces
<interface name=Entry members=Deposit Check Withdrawal properties=date amount />
Por lo tanto, el interface Entry, incluye las propiedades para los elementos
date y amount:
public int getAmount();
public void setAmount(int x);
public Date getDate();
public void setDate(Date d);
La propiedad amount devuelve y acepta un int porque
escribimos una declaración de conversión de unión para convertir desde String a
BigDecimal y utilizamos el atributo convert en la
declaración de unión del elemento amount y le asignamos el valor
BigDecimal:
<element name=amount type=value convert=BigDecimal />
<conversion name="BigDecimal" type="java.math.BigDecimal" />
El Fichero Check.java
El elemento check está unido a la clase Check, que
tiene la firma:
public interface Check extends MarshallableObject implements Element, Entry{
La clase Check extiende MarshallableObject, que es
la clase abstracta que representa cualquier objeto que se pueda empaquetar o despempaquetar pero no sea necesariamente
un elemento raíz. La clase Check debe implementar Element
porque, al contrario que MarshallableRootElement, un
MarshallableObject no tiene que ser un objeto derivado de un elemento y por lo tanto
no implementa Element en si mismo. Finalmente, Check
implementa Entry porque especificamos Entry como el
supertype del contenido del elemento transaction en la
sección Crear Interfaces. El elemento check
tiene dos atributos y contiene seis elementos:
<!ELEMENT check ( date, name, (pending | void | cleared), memo? ) >
<!ATTLIST check
number CDATA #IMPLIED
category ( rent | groceries | other ) #IMPLIED >
El elemento date está unido a una propiedad y devuelve un objeto Date:
public Date getDate();
public void setDate(Date x);
Los elementos name, y memo, que sólo contienen texto
están unidos a estas propiedades:
public String getName();
public void setName(String x);
public String getMemo();
public void setMemo(String x);
En la sección Especificar Tipos, especificamos un tipo
int para el atributo number, produciendo esta propiedad:
public int getNumber();
public void setNumber(int x);
En la sección Personalizar Declaraciones de Unión de Modelos de
Contenido especificamos que el modelo de grupo de elección,
(pending | void | cleared), está unido a una propiedad, produciendo estos métodos:
public MarshallableObject getPendVoidClrd();
public void setPendVoidClrd(MarshallableObject x);
La propiedad pend-void-clrd devuelve y acepta un
MarshallableObject, que representa objetos que pueden ser empaquetados y desempaquetado.
La razón por la que este tipo de propiedad no es un String es porque no podemos determinar
si un String se supone que es un elmento pend,
void, cleared; con MarshallableObject,
si podemos porque el MarshallableObject será un objeto Pend,
Void, o Cleared. La clase Check
también contiene una propiedad para la enumeración checkCategory, que especificamos en la sección
Crear Tipos Enumerados:
<enumeration name=CheckCategory members=rent groceries other />
...
<attribute name=category convert=CheckCategory />
La propiedad que genera está declaración de unión es:
public CheckCategory getCheckCategory();
public void setCheckCategory(CheckCategory x);
Esta propiedad acepta y devuelve un objeto CheckCategory, que es un interface de
la clase CheckCategory. Esta clase se explica en la siguiente sección.
El Fichero CheckCategory.java
El atributo category toma un valor de un conjunto fijo de valores representado por un grupo
de elección en la definición de atributo:
<!ATTLIST check ... category ( rent | groceries | other ) #IMPLIED >
Cuando seguimos las instrucciones de la sección Crear Tipos Enumerados,
especificamos una unidón de este atributo a un tipo seguro enum:
<enumeration name=CheckCategory members=rent groceries other />
...
<attribute name=category convert=CheckCategory />
Un tipo seguro enum es una clase que representa una enumeración que consta de un elemento
o un atributo y la lista de los posibles valores, sólo uno de los cuales puede asignarse a la clase. El nombre de la clase
corresponde al del elemento o del atributo, y los campos estáticos representan los valores. Las clases de tipo seguro
enum tienen muchas ventajas, incluyendo controlar el tipo en tiempo de compilación. El
tipo seguro enum generado desde el atributo category es:
public final class CheckCategory {
public final static CheckCategory RENT;
public final static CheckCategory GROCERIES;
public final static CheckCategory OTHER;
public static CheckCategory parse(String x);
public String toString();
}
El método parse intenta mapear un argumento String a uno
de los valores aceptados. El método toString devuelve el valor actual de un
CheckCategory como un String.
El Fichero Pending.java
La clase Pending representa uno de los miembros del modelo de grupo de elección
contenido en la declaración del elemento check:
<!ELEMENT check ( date, name, ( pending | void | cleared), memo? ) >
Este elemento, así como los elementos void o cleared, están unidos
a sus propias clases porque son parte de este grupo modelo de elección. Según lo explicado en el sección del fichero
Check.java, la propiedad a la que está unido este grupo debe devolver un
MarshallableObject, que debe también ser un objeto Pend,
Void o Cleared, de modo que la aplicación sepa qué elemento
encontrará durante el empaquetamiento o despempaquetamiento. Las clases Pend,
Void y Clearedtienen un constructor sin argumentos.
El siguiente capítulo nos mostrará como trabajar con estas clases para
construir representaciones de datos.